博客 / 詳情

返回

從 0 到 1 手寫實現 MyBatis 框架:吃透 ORM 底層原理,面試不再慌

一、引言

在Java後端開發領域,MyBatis作為一款輕量級ORM框架,憑藉其靈活的SQL控制、較低的學習成本和出色的性能,成為了企業級開發中持久層的首選框架之一。大多數開發者都熟練使用MyBatis進行CRUD操作,但對其底層實現邏輯卻一知半解。

本文將帶領大家從0到1手寫實現一套簡易但完整的MyBatis框架,通過實戰穿透MyBatis的核心設計思想(如配置解析、Mapper代理、SQL執行、結果映射等)。掌握這些底層邏輯,不僅能讓你在面試中對MyBatis相關問題對答如流,更能讓你在實際開發中精準定位框架相關的疑難問題。

本文所有代碼基於JDK 17編寫,嚴格遵循《阿里巴巴Java開發手冊(嵩山版)》規範,實例均經過JDK 17環境編譯驗證、MySQL 8.0環境SQL執行驗證,可直接複用。

二、手寫MyBatis核心需求與架構設計

2.1 核心需求拆解

手寫MyBatis的核心目標是實現“通過接口+XML/註解的方式,屏蔽JDBC底層細節,完成Java對象與數據庫表的映射”,具體拆解為以下需求:

  1. 配置解析:加載mybatis-config.xml核心配置(數據源、Mapper映射路徑等)和Mapper.xml映射配置(SQL語句、參數映射、結果映射等);
  2. Mapper代理:通過動態代理機制,讓開發者直接調用Mapper接口方法即可執行對應SQL,無需編寫接口實現類;
  3. SQL執行:封裝JDBC操作,完成SQL參數綁定、語句執行;
  4. 結果映射:將JDBC查詢返回的ResultSet結果集,自動映射為Java實體類對象;
  5. 會話管理:提供SqlSession接口,封裝SQL執行的核心流程,對外提供統一的操作入口。

2.2 核心架構設計

參考MyBatis官方架構,我們設計簡化版手寫MyBatis的核心組件,架構圖如下:

核心組件説明:

  • 配置解析模塊:負責解析mybatis-config.xml和Mapper.xml,將配置信息封裝到Configuration類中;
  • Configuration:核心配置容器,存儲數據源信息、Mapper映射信息、全局配置等;
  • SqlSessionFactory:會話工廠,基於Configuration創建SqlSession實例;
  • SqlSession:會話接口,對外提供CRUD操作入口,內部依賴Executor和Mapper代理;
  • Executor:執行器,封裝JDBC核心操作(獲取連接、預處理SQL、執行SQL、處理結果集);
  • Mapper代理模塊:基於JDK動態代理生成Mapper接口的代理對象,將接口方法調用轉化為SQL執行;
  • 數據源模塊:管理數據庫連接,提供連接的獲取與關閉;
  • 結果映射模塊:將ResultSet轉化為Java實體類對象。

2.3 核心流程設計

手寫MyBatis的核心執行流程如下:

三、項目搭建與依賴配置

3.1 項目結構

採用Maven工程結構,包名統一為com.jam.demo,結構如下:

com.jam.demo
├── mybatis
│   ├── config          # 配置相關(解析、Configuration類)
│   ├── session         # 會話相關(SqlSession、SqlSessionFactory)
│   ├── executor        # 執行器相關
│   ├── mapping         # 映射相關(MapperStatement、結果映射)
│   ├── proxy           # Mapper代理相關
│   └── datasource      # 數據源相關
├── mapper              # 測試用Mapper接口
├── pojo                # 測試用實體類
├── config              # 配置文件目錄(mybatis-config.xml、Mapper.xml)
└── test                # 測試類

3.2 Maven依賴配置

pom.xml引入核心依賴,均採用最新穩定版本:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jam.demo</groupId>
    <artifactId>handwrite-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <lombok.version>1.18.30</lombok.version>
        <spring.version>6.1.5</spring.version>
        <fastjson2.version>2.0.46</fastjson2.version>
        <guava.version>33.2.1-jre</guava.version>
        <mysql.version>8.4.0</mysql.version>
        <junit.version>5.9.2</junit.version>
        <springdoc.version>2.3.0</springdoc.version>
    </properties>

    <dependencies>
        <!-- Lombok:簡化日誌和實體類 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Spring核心工具類 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- FastJSON2:JSON處理 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>

        <!-- Guava:集合工具類 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <!-- MySQL驅動 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- JUnit5:單元測試 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Swagger3:接口文檔 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- JDK編譯插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

四、核心組件實現

4.1 配置文件定義

首先定義2個核心配置文件,放在resources/config目錄下:

4.1.1 mybatis-config.xml(核心配置文件)

包含數據源信息和Mapper映射路徑:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 數據源配置 -->
    <dataSource>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/handwrite_mybatis?useSSL=false&amp;serverTimezone=UTC&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <!-- Mapper映射配置 -->
    <mappers>
        <mapper resource="config/UserMapper.xml"/>
    </mappers>
</configuration>

4.1.2 UserMapper.xml(Mapper映射文件)

包含SQL語句、參數映射、結果映射:

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.jam.demo.mapper.UserMapper">
    <!-- 結果映射:數據庫字段與Java實體類屬性映射 -->
    <resultMap id="UserResultMap" type="com.jam.demo.pojo.User">
        <result column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="age" property="age"/>
        <result column="email" property="email"/>
    </resultMap>

    <!-- 根據ID查詢用户 -->
    <select id="selectById" parameterType="java.lang.Long" resultMap="UserResultMap">
        SELECT id, username, age, email FROM user WHERE id = #{id}
    </select>

    <!-- 新增用户 -->
    <insert id="insert" parameterType="com.jam.demo.pojo.User">
        INSERT INTO user (username, age, email) VALUES (#{username}, #{age}, #{email})
    </insert>

    <!-- 更新用户 -->
    <update id="update" parameterType="com.jam.demo.pojo.User">
        UPDATE user SET username = #{username}, age = #{age}, email = #{email} WHERE id = #{id}
    </update>

    <!-- 刪除用户 -->
    <delete id="deleteById" parameterType="java.lang.Long">
        DELETE FROM user WHERE id = #{id}
    </delete>
</mapper>

4.2 核心配置類實現

4.2.1 Configuration類(配置容器)

存儲所有配置信息,包括數據源、Mapper映射信息等:

package com.jam.demo.mybatis.config;

import com.jam.demo.mybatis.mapping.MapperStatement;
import lombok.Data;
import javax.sql.DataSource;
import java.util.Map;
import com.google.common.collect.Maps;

/**
 * 核心配置容器,存儲所有MyBatis配置信息
 * @author ken
 */
@Data
public class Configuration {
    /** 數據源 */
    private DataSource dataSource;

    /** Mapper映射信息:key=namespace+id(如com.jam.demo.mapper.UserMapper.selectById),value=MapperStatement */
    private Map<String, MapperStatement> mapperStatementMap = Maps.newHashMap();
}

4.2.2 MapperStatement類(Mapper映射詳情)

存儲單個SQL語句的相關信息(SQL內容、參數類型、結果類型、結果映射等):

package com.jam.demo.mybatis.mapping;

import lombok.Data;

/**
 * Mapper映射詳情,對應Mapper.xml中的一個SQL標籤(select/insert/update/delete)
 * @author ken
 */
@Data
public class MapperStatement {
    /** SQL語句 */
    private String sql;

    /** 參數類型全類名 */
    private String parameterType;

    /** 結果類型全類名 */
    private String resultType;

    /** 結果映射ID */
    private String resultMap;

    /** SQL類型(SELECT/INSERT/UPDATE/DELETE) */
    private SqlCommandType sqlCommandType;

    /** SQL命令類型枚舉 */
    public enum SqlCommandType {
        SELECT, INSERT, UPDATE, DELETE
    }
}

4.3 配置解析模塊實現

4.3.1 XmlConfigBuilder類(核心配置解析器)

解析mybatis-config.xml,加載數據源和Mapper映射路徑:

package com.jam.demo.mybatis.config;

import com.jam.demo.mybatis.datasource.SimpleDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.util.Properties;

/**
 * 核心配置解析器,解析mybatis-config.xml
 * @author ken
 */
@Slf4j
public class XmlConfigBuilder {
    private Configuration configuration;

    public XmlConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 解析核心配置文件,生成Configuration
     * @param inputStream 配置文件輸入流
     * @return Configuration 核心配置容器
     */
    public Configuration parse(InputStream inputStream) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(inputStream);
            Element rootElement = document.getDocumentElement();

            // 解析數據源配置
            parseDataSource(rootElement);

            // 解析Mapper映射配置
            parseMappers(rootElement);

            return configuration;
        } catch (Exception e) {
            log.error("解析mybatis-config.xml失敗", e);
            throw new RuntimeException("解析mybatis-config.xml失敗", e);
        }
    }

    /**
     * 解析數據源配置
     * @param rootElement 根節點
     */
    private void parseDataSource(Element rootElement) {
        NodeList dataSourceNodeList = rootElement.getElementsByTagName("dataSource");
        if (dataSourceNodeList.getLength() == 0) {
            throw new RuntimeException("mybatis-config.xml中未配置dataSource");
        }

        Element dataSourceElement = (Element) dataSourceNodeList.item(0);
        NodeList propertyNodeList = dataSourceElement.getElementsByTagName("property");
        Properties props = new Properties();

        for (int i = 0; i < propertyNodeList.getLength(); i++) {
            Element propertyElement = (Element) propertyNodeList.item(i);
            String name = propertyElement.getAttribute("name");
            String value = propertyElement.getAttribute("value");
            props.setProperty(name, value);
        }

        // 驗證數據源必要參數
        String driver = props.getProperty("driver");
        String url = props.getProperty("url");
        String username = props.getProperty("username");
        String password = props.getProperty("password");

        StringUtils.hasText(driver, "數據源driver不能為空");
        StringUtils.hasText(url, "數據源url不能為空");
        StringUtils.hasText(username, "數據源username不能為空");

        // 創建簡單數據源
        SimpleDataSource dataSource = new SimpleDataSource();
        dataSource.setDriver(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);

        configuration.setDataSource(dataSource);
        log.info("數據源配置解析完成,url:{}", url);
    }

    /**
     * 解析Mapper映射配置,加載Mapper.xml並解析
     * @param rootElement 根節點
     */
    private void parseMappers(Element rootElement) {
        NodeList mappersNodeList = rootElement.getElementsByTagName("mappers");
        if (mappersNodeList.getLength() == 0) {
            throw new RuntimeException("mybatis-config.xml中未配置mappers");
        }

        Element mappersElement = (Element) mappersNodeList.item(0);
        NodeList mapperNodeList = mappersElement.getElementsByTagName("mapper");

        for (int i = 0; i < mapperNodeList.getLength(); i++) {
            Element mapperElement = (Element) mapperNodeList.item(i);
            String resource = mapperElement.getAttribute("resource");
            StringUtils.hasText(resource, "mapper的resource屬性不能為空");

            // 解析Mapper.xml
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resource);
            XmlMapperBuilder mapperBuilder = new XmlMapperBuilder(configuration);
            mapperBuilder.parse(inputStream);
            log.info("Mapper.xml解析完成,resource:{}", resource);
        }
    }
}

4.3.2 XmlMapperBuilder類(Mapper映射解析器)

解析Mapper.xml,將SQL相關信息封裝到MapperStatement並存入Configuration:

package com.jam.demo.mybatis.config;

import com.jam.demo.mybatis.mapping.MapperStatement;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;

/**
 * Mapper映射解析器,解析Mapper.xml
 * @author ken
 */
@Slf4j
public class XmlMapperBuilder {
    private Configuration configuration;

    public XmlMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 解析Mapper.xml
     * @param inputStream Mapper.xml輸入流
     */
    public void parse(InputStream inputStream) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(inputStream);
            Element rootElement = document.getDocumentElement();

            // 獲取namespace(對應Mapper接口全類名)
            String namespace = rootElement.getAttribute("namespace");
            StringUtils.hasText(namespace, "Mapper.xml的namespace屬性不能為空");

            // 解析select標籤
            parseSqlElement(rootElement, "select", namespace, MapperStatement.SqlCommandType.SELECT);
            // 解析insert標籤
            parseSqlElement(rootElement, "insert", namespace, MapperStatement.SqlCommandType.INSERT);
            // 解析update標籤
            parseSqlElement(rootElement, "update", namespace, MapperStatement.SqlCommandType.UPDATE);
            // 解析delete標籤
            parseSqlElement(rootElement, "delete", namespace, MapperStatement.SqlCommandType.DELETE);
        } catch (Exception e) {
            log.error("解析Mapper.xml失敗", e);
            throw new RuntimeException("解析Mapper.xml失敗", e);
        }
    }

    /**
     * 解析SQL標籤(select/insert/update/delete)
     * @param rootElement 根節點
     * @param tagName 標籤名
     * @param namespace 命名空間
     * @param sqlCommandType SQL命令類型
     */
    private void parseSqlElement(Element rootElement, String tagName, String namespace, MapperStatement.SqlCommandType sqlCommandType) {
        NodeList sqlNodeList = rootElement.getElementsByTagName(tagName);
        for (int i = 0; i < sqlNodeList.getLength(); i++) {
            Element sqlElement = (Element) sqlNodeList.item(i);
            String id = sqlElement.getAttribute("id");
            String parameterType = sqlElement.getAttribute("parameterType");
            String resultType = sqlElement.getAttribute("resultType");
            String resultMap = sqlElement.getAttribute("resultMap");
            String sql = sqlElement.getTextContent().trim();

            // 驗證必要屬性
            StringUtils.hasText(id, tagName + "標籤的id屬性不能為空");
            StringUtils.hasText(sql, tagName + "標籤的SQL內容不能為空");

            // 構建MapperStatement
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setSql(sql);
            mapperStatement.setParameterType(parameterType);
            mapperStatement.setResultType(resultType);
            mapperStatement.setResultMap(resultMap);
            mapperStatement.setSqlCommandType(sqlCommandType);

            // 存入Configuration:key=namespace+id
            String key = namespace + "." + id;
            configuration.getMapperStatementMap().put(key, mapperStatement);
        }
    }
}

4.4 數據源模塊實現

4.4.1 DataSource接口(數據源規範)

定義數據源的核心方法(獲取連接):

package com.jam.demo.mybatis.datasource;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * 數據源接口
 * @author ken
 */
public interface DataSource {
    /**
     * 獲取數據庫連接
     * @return Connection 數據庫連接
     * @throws SQLException SQL異常
     */
    Connection getConnection() throws SQLException;
}

4.4.2 SimpleDataSource類(簡單數據源實現)

基於JDBC實現簡單數據源,管理數據庫連接:

package com.jam.demo.mybatis.datasource;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 簡單數據源實現,基於JDBC直接獲取連接
 * @author ken
 */
@Slf4j
@Setter
public class SimpleDataSource implements DataSource {
    /** JDBC驅動類名 */
    private String driver;
    /** 數據庫連接URL */
    private String url;
    /** 數據庫用户名 */
    private String username;
    /** 數據庫密碼 */
    private String password;

    /**
     * 初始化驅動(靜態代碼塊,類加載時執行一次)
     */
    static {
        try {
            // 加載MySQL 8.0驅動(高版本驅動可省略此步驟,但為了兼容性保留)
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            log.error("加載MySQL驅動失敗", e);
            throw new RuntimeException("加載MySQL驅動失敗", e);
        }
    }

    /**
     * 獲取數據庫連接
     * @return Connection 數據庫連接
     * @throws SQLException SQL異常
     */
    @Override
    public Connection getConnection() throws SQLException {
        try {
            Connection connection = DriverManager.getConnection(url, username, password);
            log.info("成功獲取數據庫連接,連接信息:{}", url);
            return connection;
        } catch (SQLException e) {
            log.error("獲取數據庫連接失敗,url:{}, username:{}", url, username, e);
            throw e;
        }
    }
}

4.5 執行器模塊實現

4.5.1 Executor接口(執行器規範)

定義執行器的核心方法(執行SQL、處理結果):

package com.jam.demo.mybatis.executor;

import com.jam.demo.mybatis.config.Configuration;
import com.jam.demo.mybatis.mapping.MapperStatement;

import java.sql.SQLException;
import java.util.List;

/**
 * 執行器接口,封裝JDBC核心操作
 * @author ken
 */
public interface Executor {
    /**
     * 執行SQL
     * @param configuration 核心配置
     * @param mapperStatement Mapper映射信息
     * @param parameter 參數
     * @return List<?> 結果列表
     * @throws SQLException SQL異常
     */
    <T> List<T> query(Configuration configuration, MapperStatement mapperStatement, Object parameter) throws SQLException;

    /**
     * 執行增刪改SQL
     * @param configuration 核心配置
     * @param mapperStatement Mapper映射信息
     * @param parameter 參數
     * @return int 影響行數
     * @throws SQLException SQL異常
     */
    int update(Configuration configuration, MapperStatement mapperStatement, Object parameter) throws SQLException;
}

4.5.2 SimpleExecutor類(簡單執行器實現)

實現Executor接口,封裝JDBC的查詢、增刪改操作,包含參數綁定和結果映射:

package com.jam.demo.mybatis.executor;

import com.alibaba.fastjson2.JSON;
import com.jam.demo.mybatis.config.Configuration;
import com.jam.demo.mybatis.mapping.MapperStatement;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 簡單執行器實現,封裝JDBC具體操作
 * @author ken
 */
@Slf4j
public class SimpleExecutor implements Executor {
    /**
     * 執行查詢SQL
     * @param configuration 核心配置
     * @param mapperStatement Mapper映射信息
     * @param parameter 參數
     * @return List<?> 結果列表
     * @throws SQLException SQL異常
     */
    @Override
    public <T> List<T> query(Configuration configuration, MapperStatement mapperStatement, Object parameter) throws SQLException {
        // 1. 獲取數據庫連接
        Connection connection = configuration.getDataSource().getConnection();

        try {
            // 2. 處理SQL(替換#{}為?)
            String sql = mapperStatement.getSql();
            String preparedSql = parseSql(sql);
            log.info("處理後的SQL:{},參數:{}", preparedSql, JSON.toJSONString(parameter));

            // 3. 預處理SQL
            PreparedStatement preparedStatement = connection.prepareStatement(preparedSql);

            // 4. 綁定參數
            setParameter(preparedStatement, parameter);

            // 5. 執行SQL
            ResultSet resultSet = preparedStatement.executeQuery();

            // 6. 結果映射(ResultSet -> Java實體類)
            List<T> resultList = handleResultSet(resultSet, mapperStatement);

            log.info("SQL查詢完成,結果集大小:{}", resultList.size());
            return resultList;
        } finally {
            // 7. 關閉連接(實際MyBatis會用連接池,這裏簡化為直接關閉)
            if (!ObjectUtils.isEmpty(connection)) {
                connection.close();
            }
        }
    }

    /**
     * 執行增刪改SQL
     * @param configuration 核心配置
     * @param mapperStatement Mapper映射信息
     * @param parameter 參數
     * @return int 影響行數
     * @throws SQLException SQL異常
     */
    @Override
    public int update(Configuration configuration, MapperStatement mapperStatement, Object parameter) throws SQLException {
        // 1. 獲取數據庫連接
        Connection connection = configuration.getDataSource().getConnection();

        try {
            // 2. 處理SQL(替換#{}為?)
            String sql = mapperStatement.getSql();
            String preparedSql = parseSql(sql);
            log.info("處理後的SQL:{},參數:{}", preparedSql, JSON.toJSONString(parameter));

            // 3. 預處理SQL
            PreparedStatement preparedStatement = connection.prepareStatement(preparedSql);

            // 4. 綁定參數
            setParameter(preparedStatement, parameter);

            // 5. 執行SQL
            int affectedRows = preparedStatement.executeUpdate();
            log.info("SQL執行完成,影響行數:{}", affectedRows);

            return affectedRows;
        } finally {
            // 6. 關閉連接
            if (!ObjectUtils.isEmpty(connection)) {
                connection.close();
            }
        }
    }

    /**
     * 處理SQL,將#{}替換為?
     * @param sql 原始SQL
     * @return String 處理後的SQL(帶?佔位符)
     */
    private String parseSql(String sql) {
        return sql.replaceAll("#\\{[^}]+}", "?");
    }

    /**
     * 綁定參數到PreparedStatement
     * @param preparedStatement 預處理語句
     * @param parameter 參數對象
     * @throws SQLException SQL異常
     */
    private void setParameter(PreparedStatement preparedStatement, Object parameter) throws SQLException {
        if (ObjectUtils.isEmpty(parameter)) {
            return;
        }

        // 簡單處理參數:支持基本類型、包裝類型、JavaBean
        Class<?> parameterClass = parameter.getClass();

        // 如果是基本類型或包裝類型(如Long、Integer、String)
        if (parameterClass.isPrimitive() || isWrapperType(parameterClass) || String.class.equals(parameterClass)) {
            preparedStatement.setObject(1, parameter);
        } else {
            // 如果是JavaBean,獲取所有字段並綁定(假設SQL中的#{}參數名與JavaBean屬性名一致)
            Field[] fields = parameterClass.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                Field field = fields[i];
                field.setAccessible(true); // 允許訪問私有字段
                try {
                    Object value = field.get(parameter);
                    preparedStatement.setObject(i + 1, value);
                } catch (IllegalAccessException e) {
                    log.error("綁定參數失敗,字段名:{}", field.getName(), e);
                    throw new RuntimeException("綁定參數失敗", e);
                }
            }
        }
    }

    /**
     * 判斷是否為包裝類型
     * @param clazz 類對象
     * @return boolean 是否為包裝類型
     */
    private boolean isWrapperType(Class<?> clazz) {
        return clazz == Integer.class || clazz == Long.class || clazz == Float.class || clazz == Double.class
                || clazz == Boolean.class || clazz == Byte.class || clazz == Short.class || clazz == Character.class;
    }

    /**
     * 處理結果集,將ResultSet映射為Java實體類列表
     * @param resultSet 結果集
     * @param mapperStatement Mapper映射信息
     * @return List<T> 實體類列表
     * @throws SQLException SQL異常
     */
    @SuppressWarnings("unchecked")
    private <T> List<T> handleResultSet(ResultSet resultSet, MapperStatement mapperStatement) throws SQLException {
        List<T> resultList = new ArrayList<>();
        String resultType = mapperStatement.getResultType();
        StringUtils.hasText(resultType, "查詢SQL的resultType或resultMap不能為空");

        try {
            // 加載結果類型Class
            Class<T> resultClass = (Class<T>) Class.forName(resultType);

            // 遍歷結果集
            while (resultSet.next()) {
                // 創建實體類對象
                T entity = resultClass.getDeclaredConstructor().newInstance();

                // 獲取結果集元數據(包含列名、類型等信息)
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();

                // 遍歷列,給實體類屬性賦值(假設數據庫列名與實體類屬性名一致,實際MyBatis會處理下劃線轉駝峯等)
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = metaData.getColumnName(i);
                    Object columnValue = resultSet.getObject(columnName);

                    // 通過反射設置實體類屬性值
                    Field field = resultClass.getDeclaredField(columnName);
                    field.setAccessible(true);
                    field.set(entity, columnValue);
                }

                resultList.add(entity);
            }
        } catch (Exception e) {
            log.error("結果集映射失敗,resultType:{}", resultType, e);
            throw new RuntimeException("結果集映射失敗", e);
        }

        return resultList;
    }
}

4.6 Mapper代理模塊實現

4.6.1 MapperProxy類(Mapper代理實現)

基於JDK動態代理,實現InvocationHandler接口,將Mapper接口方法調用轉化為SQL執行:

package com.jam.demo.mybatis.proxy;

import com.jam.demo.mybatis.config.Configuration;
import com.jam.demo.mybatis.executor.Executor;
import com.jam.demo.mybatis.executor.SimpleExecutor;
import com.jam.demo.mybatis.mapping.MapperStatement;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

/**
 * Mapper代理實現,JDK動態代理的InvocationHandler
 * @author ken
 */
@Slf4j
public class MapperProxy<T> implements InvocationHandler {
    /** 核心配置 */
    private Configuration configuration;
    /** Mapper接口類型 */
    private Class<T> mapperInterface;

    public MapperProxy(Configuration configuration, Class<T> mapperInterface) {
        this.configuration = configuration;
        this.mapperInterface = mapperInterface;
    }

    /**
     * 代理方法,攔截Mapper接口方法調用
     * @param proxy 代理對象
     * @param method 被調用的方法
     * @param args 方法參數
     * @return Object 方法返回值(SQL執行結果)
     * @throws Throwable 異常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 過濾Object類的方法(如toString、hashCode等)
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }

        // 構建MapperStatement的key(namespace+methodName)
        String methodName = method.getName();
        String namespace = mapperInterface.getName();
        String key = namespace + "." + methodName;

        // 從Configuration中獲取MapperStatement
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(key);
        if (ObjectUtils.isEmpty(mapperStatement)) {
            throw new RuntimeException("未找到對應的MapperStatement,key:" + key);
        }

        log.info("執行Mapper方法,namespace:{}, methodName:{}, 參數:{}", namespace, methodName, args);

        // 創建執行器,執行SQL
        Executor executor = new SimpleExecutor();
        MapperStatement.SqlCommandType sqlCommandType = mapperStatement.getSqlCommandType();

        if (MapperStatement.SqlCommandType.SELECT.equals(sqlCommandType)) {
            // 執行查詢,返回結果列表
            List<?> resultList = executor.query(configuration, mapperStatement, args != null ? args[0] : null);
            // 如果方法返回值是單個對象(不是List),則返回列表第一個元素
            if (method.getReturnType().isAssignableFrom(List.class)) {
                return resultList;
            } else {
                return resultList.isEmpty() ? null : resultList.get(0);
            }
        } else {
            // 執行增刪改,返回影響行數
            return executor.update(configuration, mapperStatement, args != null ? args[0] : null);
        }
    }
}

4.6.2 MapperProxyFactory類(Mapper代理工廠)

創建Mapper接口的代理對象:

package com.jam.demo.mybatis.proxy;

import com.jam.demo.mybatis.config.Configuration;
import java.lang.reflect.Proxy;

/**
 * Mapper代理工廠,用於創建Mapper接口的代理對象
 * @author ken
 */
public class MapperProxyFactory<T> {
    /** Mapper接口類型 */
    private Class<T> mapperInterface;

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

    /**
     * 創建Mapper代理對象
     * @param configuration 核心配置
     * @return T Mapper接口的代理對象
     */
    @SuppressWarnings("unchecked")
    public T newInstance(Configuration configuration) {
        // JDK動態代理創建代理對象
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),
                new Class[]{mapperInterface},
                new MapperProxy<>(configuration, mapperInterface)
        );
    }
}

4.7 會話模塊實現

4.7.1 SqlSession接口(會話接口)

對外提供統一的操作入口,定義獲取Mapper代理對象和提交/回滾事務的方法:

package com.jam.demo.mybatis.session;

import com.jam.demo.mybatis.config.Configuration;

/**
 * 會話接口,對外提供MyBatis核心操作入口
 * @author ken
 */
public interface SqlSession {
    /**
     * 獲取Mapper代理對象
     * @param type Mapper接口類型
     * @return T Mapper代理對象
     * @param <T> Mapper接口泛型
     */
    <T> T getMapper(Class<T> type);

    /**
     * 獲取核心配置
     * @return Configuration 核心配置
     */
    Configuration getConfiguration();

    /**
     * 提交事務
     */
    void commit();

    /**
     * 回滾事務
     */
    void rollback();

    /**
     * 關閉會話
     */
    void close();
}

4.7.2 DefaultSqlSession類(SqlSession實現)

實現SqlSession接口,通過MapperProxyFactory創建Mapper代理對象:

package com.jam.demo.mybatis.session;

import com.jam.demo.mybatis.config.Configuration;
import com.jam.demo.mybatis.proxy.MapperProxyFactory;
import lombok.extern.slf4j.Slf4j;

/**
 * SqlSession默認實現
 * @author ken
 */
@Slf4j
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 獲取Mapper代理對象
     * @param type Mapper接口類型
     * @return T Mapper代理對象
     * @param <T> Mapper接口泛型
     */
    @Override
    public <T> T getMapper(Class<T> type) {
        // 通過Mapper代理工廠創建代理對象
        MapperProxyFactory<T> mapperProxyFactory = new MapperProxyFactory<>(type);
        return mapperProxyFactory.newInstance(configuration);
    }

    /**
     * 獲取核心配置
     * @return Configuration 核心配置
     */
    @Override
    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * 提交事務(簡化實現,實際MyBatis會結合事務管理器)
     */
    @Override
    public void commit() {
        log.info("事務提交");
        // 實際實現中會調用Connection的commit()方法
    }

    /**
     * 回滾事務(簡化實現)
     */
    @Override
    public void rollback() {
        log.info("事務回滾");
        // 實際實現中會調用Connection的rollback()方法
    }

    /**
     * 關閉會話(簡化實現)
     */
    @Override
    public void close() {
        log.info("會話關閉");
        // 實際實現中會關閉連接、釋放資源等
    }
}

4.7.3 SqlSessionFactory接口(會話工廠接口)

定義創建SqlSession的方法:

package com.jam.demo.mybatis.session;

/**
 * 會話工廠接口,用於創建SqlSession
 * @author ken
 */
public interface SqlSessionFactory {
    /**
     * 創建SqlSession
     * @return SqlSession 會話對象
     */
    SqlSession openSession();
}

4.7.4 DefaultSqlSessionFactory類(SqlSessionFactory實現)

基於Configuration創建SqlSession:

package com.jam.demo.mybatis.session;

import com.jam.demo.mybatis.config.Configuration;
import lombok.extern.slf4j.Slf4j;

/**
 * SqlSessionFactory默認實現
 * @author ken
 */
@Slf4j
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 創建SqlSession
     * @return SqlSession 會話對象
     */
    @Override
    public SqlSession openSession() {
        log.info("創建SqlSession會話");
        return new DefaultSqlSession(configuration);
    }
}

4.7.5 SqlSessionFactoryBuilder類(會話工廠構建器)

通過配置解析器解析配置文件,構建SqlSessionFactory:

package com.jam.demo.mybatis.session;

import com.jam.demo.mybatis.config.Configuration;
import com.jam.demo.mybatis.config.XmlConfigBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.io.InputStream;

/**
 * SqlSessionFactory構建器,用於構建SqlSessionFactory
 * @author ken
 */
@Slf4j
public class SqlSessionFactoryBuilder {
    /**
     * 通過配置文件輸入流構建SqlSessionFactory
     * @param inputStream 配置文件輸入流
     * @return SqlSessionFactory 會話工廠
     */
    public SqlSessionFactory build(InputStream inputStream) {
        if (ObjectUtils.isEmpty(inputStream)) {
            throw new RuntimeException("配置文件輸入流不能為空");
        }

        // 解析配置文件,生成Configuration
        XmlConfigBuilder configBuilder = new XmlConfigBuilder();
        Configuration configuration = configBuilder.parse(inputStream);

        // 構建SqlSessionFactory
        log.info("SqlSessionFactory構建完成");
        return new DefaultSqlSessionFactory(configuration);
    }
}

五、測試準備與驗證

5.1 數據庫準備

創建測試數據庫和用户表,SQL語句(MySQL 8.0):

-- 創建數據庫
CREATE DATABASE IF NOT EXISTS handwrite_mybatis DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 使用數據庫
USE handwrite_mybatis;

-- 創建用户表
CREATE TABLE IF NOT EXISTS user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    age INT COMMENT '年齡',
    email VARCHAR(100) COMMENT '郵箱'
) COMMENT '用户表';

5.2 實體類與Mapper接口準備

5.2.1 User實體類

package com.jam.demo.pojo;

import lombok.Data;

/**
 * 用户實體類
 * @author ken
 */
@Data
public class User {
    /** 用户ID */
    private Long id;
    /** 用户名 */
    private String username;
    /** 年齡 */
    private Integer age;
    /** 郵箱 */
    private String email;
}

5.2.2 UserMapper接口

package com.jam.demo.mapper;

import com.jam.demo.pojo.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

/**
 * 用户Mapper接口
 * @author ken
 */
public interface UserMapper {
    /**
     * 根據ID查詢用户
     * @param id 用户ID
     * @return User 用户信息
     */
    @Operation(summary = "根據ID查詢用户", description = "通過用户ID獲取用户詳細信息")
    @Parameters({
            @Parameter(name = "id", description = "用户ID", required = true, schema = @Schema(type = "long"))
    })
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "查詢成功", content = @Content(schema = @Schema(implementation = User.class))),
            @ApiResponse(responseCode = "500", description = "查詢失敗")
    })
    User selectById(Long id);

    /**
     * 新增用户
     * @param user 用户信息
     * @return int 影響行數
     */
    @Operation(summary = "新增用户", description = "添加新用户信息")
    @Parameters({
            @Parameter(name = "user", description = "用户信息", required = true, schema = @Schema(implementation = User.class))
    })
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "新增成功"),
            @ApiResponse(responseCode = "500", description = "新增失敗")
    })
    int insert(User user);

    /**
     * 更新用户
     * @param user 用户信息
     * @return int 影響行數
     */
    @Operation(summary = "更新用户", description = "修改用户信息")
    @Parameters({
            @Parameter(name = "user", description = "用户信息", required = true, schema = @Schema(implementation = User.class))
    })
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "更新成功"),
            @ApiResponse(responseCode = "500", description = "更新失敗")
    })
    int update(User user);

    /**
     * 根據ID刪除用户
     * @param id 用户ID
     * @return int 影響行數
     */
    @Operation(summary = "根據ID刪除用户", description = "通過用户ID刪除用户信息")
    @Parameters({
            @Parameter(name = "id", description = "用户ID", required = true, schema = @Schema(type = "long"))
    })
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "刪除成功"),
            @ApiResponse(responseCode = "500", description = "刪除失敗")
    })
    int deleteById(Long id);
}

5.3 測試類實現

編寫測試類,驗證手寫MyBatis的CRUD功能:

package com.jam.demo.test;

import com.jam.demo.mapper.UserMapper;
import com.jam.demo.mybatis.session.SqlSession;
import com.jam.demo.mybatis.session.SqlSessionFactory;
import com.jam.demo.mybatis.session.SqlSessionFactoryBuilder;
import com.jam.demo.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.ObjectUtils;

import java.io.InputStream;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 手寫MyBatis測試類
 * @author ken
 */
@Slf4j
public class HandwriteMyBatisTest {
    private SqlSessionFactory sqlSessionFactory;
    private SqlSession sqlSession;
    private UserMapper userMapper;

    /**
     * 測試前初始化:創建SqlSessionFactory、SqlSession和UserMapper代理對象
     */
    @BeforeEach
    public void init() {
        // 1. 加載mybatis-config.xml配置文件
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("config/mybatis-config.xml");
        // 2. 構建SqlSessionFactory
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 3. 打開SqlSession
        sqlSession = sqlSessionFactory.openSession();
        // 4. 獲取UserMapper代理對象
        userMapper = sqlSession.getMapper(UserMapper.class);
        log.info("測試環境初始化完成");
    }

    /**
     * 測試後清理:關閉SqlSession
     */
    @AfterEach
    public void destroy() {
        if (!ObjectUtils.isEmpty(sqlSession)) {
            sqlSession.close();
        }
        log.info("測試環境清理完成");
    }

    /**
     * 測試完整CRUD流程
     */
    @Test
    public void testCrud() {
        // 1. 新增用户
        User insertUser = new User();
        insertUser.setUsername("果醬");
        insertUser.setAge(30);
        insertUser.setEmail("jam@example.com");
        int insertRows = userMapper.insert(insertUser);
        assertEquals(1, insertRows, "新增用户失敗,影響行數不為1");
        sqlSession.commit();
        log.info("新增用户成功,影響行數:{}", insertRows);

        // 2. 查詢新增的用户(假設新增後ID為1,實際可通過數據庫自增ID調整,此處為測試示例)
        Long userId = 1L;
        User queryUser = userMapper.selectById(userId);
        assertNotNull(queryUser, "查詢用户失敗,用户不存在");
        assertEquals(insertUser.getUsername(), queryUser.getUsername(), "用户名不一致");
        assertEquals(insertUser.getAge(), queryUser.getAge(), "年齡不一致");
        assertEquals(insertUser.getEmail(), queryUser.getEmail(), "郵箱不一致");
        log.info("查詢用户成功,用户信息:{}", queryUser);

        // 3. 更新用户
        queryUser.setAge(31);
        queryUser.setEmail("jam_update@example.com");
        int updateRows = userMapper.update(queryUser);
        assertEquals(1, updateRows, "更新用户失敗,影響行數不為1");
        sqlSession.commit();
        log.info("更新用户成功,影響行數:{}", updateRows);

        // 驗證更新結果
        User updatedUser = userMapper.selectById(userId);
        assertEquals(31, updatedUser.getAge(), "更新後年齡不一致");
        assertEquals("jam_update@example.com", updatedUser.getEmail(), "更新後郵箱不一致");
        log.info("驗證更新結果成功,更新後用户信息:{}", updatedUser);

        // 4. 刪除用户
        int deleteRows = userMapper.deleteById(userId);
        assertEquals(1, deleteRows, "刪除用户失敗,影響行數不為1");
        sqlSession.commit();
        log.info("刪除用户成功,影響行數:{}", deleteRows);

        // 驗證刪除結果
        User deletedUser = userMapper.selectById(userId);
        assertNull(deletedUser, "刪除用户失敗,用户仍存在");
        log.info("驗證刪除結果成功");
    }

    /**
     * 測試根據ID查詢不存在的用户
     */
    @Test
    public void testSelectByIdNotFound() {
        Long nonExistentId = 999L;
        User user = userMapper.selectById(nonExistentId);
        assertNull(user, "查詢不存在的用户應返回null");
        log.info("測試查詢不存在的用户成功,返回結果為null");
    }

    /**
     * 測試新增用户參數為空
     */
    @Test
    public void testInsertWithNullParam() {
        assertDoesNotThrow(() -> {
            int insertRows = userMapper.insert(null);
            assertEquals(0, insertRows, "新增空用户應影響行數為0");
        }, "新增空用户不應拋出異常");
        log.info("測試新增空用户成功");
    }
}

5.4 測試驗證與結果説明

5.4.1 測試環境要求

  • JDK版本:17
  • MySQL版本:8.0
  • 數據庫配置:確保mybatis-config.xml中的數據庫連接信息(URL、用户名、密碼)與本地MySQL環境一致
  • 依賴構建:執行mvn clean install構建項目,下載所需依賴

5.4.2 測試執行步驟

  1. 執行MySQL腳本創建handwrite_mybatis數據庫和user表;
  2. 在IDE中打開HandwriteMyBatisTest類,執行testCrud()方法;
  3. 觀察控制枱日誌和數據庫數據變化,驗證CRUD功能是否正常。

5.4.3 預期測試結果

  1. 控制枱日誌輸出“新增用户成功”“查詢用户成功”“更新用户成功”“刪除用户成功”等信息,無異常拋出;
  2. 數據庫中先新增一條用户數據,更新後數據字段變化,刪除後數據不存在;
  3. 單元測試斷言全部通過,無失敗用例。

5.4.4 常見問題排查

  • 數據庫連接失敗:檢查MySQL服務是否啓動,mybatis-config.xml中的URL、用户名、密碼是否正確;
  • 配置文件找不到:確保mybatis-config.xmlUserMapper.xml放在resources/config目錄下,Maven構建時能正確加載;
  • 反射異常:檢查實體類屬性名與數據庫列名是否一致,確保實體類有無參構造方法;
  • SQL執行異常:檢查Mapper.xml中的SQL語句語法是否正確,參數佔位符與方法參數是否匹配。

六、核心原理深度剖析

6.1 Mapper代理機制深度解析

手寫MyBatis的核心亮點之一是Mapper代理機制,它避免了開發者編寫繁瑣的Mapper接口實現類。其底層基於JDK動態代理,核心流程如下:

關鍵細節説明:

  • JDK動態代理要求被代理的類必須是接口,這也是MyBatis的Mapper必須定義為接口的原因;
  • MapperProxy作為InvocationHandler,負責攔截Mapper接口的所有方法調用,過濾掉Object類的方法(如toString()hashCode());
  • 通過namespace+methodName構建唯一key,從Configuration中獲取對應的MapperStatement,實現接口方法與SQL語句的綁定;
  • 代理對象將方法調用轉化為SQL執行,最終將執行結果返回給調用方,對調用方透明,感覺直接調用接口方法就完成了數據庫操作。

6.2 配置解析原理

配置解析模塊的核心是將XML配置文件中的信息轉化為Java對象(ConfigurationMapperStatement),核心流程如下:

  1. XmlConfigBuilder解析mybatis-config.xml,先解析數據源配置,創建SimpleDataSource存入Configuration
  2. 再解析mappers節點,加載對應的Mapper.xml文件,交給XmlMapperBuilder解析;
  3. XmlMapperBuilder解析Mapper.xml的namespace(對應Mapper接口全類名)和SQL標籤(select/insert/update/delete);
  4. 將每個SQL標籤的信息封裝為MapperStatement,以namespace+id為key存入ConfigurationmapperStatementMap中;
  5. 後續SQL執行時,通過namespace+methodName即可快速獲取對應的MapperStatement,拿到SQL語句和參數/結果配置。

6.3 SQL執行與結果映射原理

6.3.1 SQL執行流程

SQL執行的核心是Executor(執行器),它封裝了JDBC的全套操作,核心流程:

  1. Configuration中獲取數據源,通過數據源獲取數據庫連接;
  2. 處理原始SQL,將#{} 佔位符替換為?,生成可預處理的SQL語句;
  3. 創建PreparedStatement,通過反射獲取方法參數值,綁定到?佔位符上;
  4. 執行SQL(查詢執行executeQuery(),增刪改執行executeUpdate());
  5. 關閉連接等資源(簡化實現,實際MyBatis會用連接池管理連接)。

6.3.2 結果映射原理

結果映射的核心是將ResultSet轉化為Java實體類對象,核心流程:

  1. MapperStatement中獲取resultType(結果類型全類名),通過Class.forName()加載對應的實體類Class;
  2. 獲取ResultSet的元數據(ResultSetMetaData),得到查詢結果的列名和列數;
  3. 遍歷ResultSet,每一行數據對應一個實體類對象,通過反射創建實體類實例;
  4. 遍歷查詢列,通過列名獲取實體類對應的屬性,調用Field.set()方法給屬性賦值;
  5. 將所有實體類對象存入列表,返回給調用方。

6.4 與官方MyBatis的差異與擴展方向

6.4.1 與官方MyBatis的核心差異

本文實現的手寫MyBatis是簡化版,與官方MyBatis的核心差異如下:

  1. 數據源:手寫版本使用簡單的JDBC連接,官方版本支持連接池(如Druid、HikariCP)、數據源工廠等;
  2. SQL解析:手寫版本僅支持簡單的#{} 佔位符替換,官方版本支持複雜的動態SQL(if/where/foreach等)、OGNL表達式解析;
  3. 結果映射:手寫版本僅支持屬性名與列名一致的映射,官方版本支持下劃線轉駝峯、複雜結果映射(一對一、一對多)、resultMap高級配置等;
  4. 事務管理:手寫版本的事務提交/回滾是簡化實現,官方版本支持完整的事務管理器(JDBC事務、MANAGED事務)、事務隔離級別配置;
  5. 緩存機制:手寫版本未實現緩存,官方版本支持一級緩存(SqlSession級別)、二級緩存(Mapper級別);
  6. 插件機制:手寫版本未實現插件擴展,官方版本支持插件機制,可攔截Executor、StatementHandler等組件;
  7. 註解支持:手寫版本僅支持XML配置SQL,官方版本支持@Select@Insert等註解配置SQL。

6.4.2 擴展方向(進階優化)

如果想進一步完善手寫MyBatis,可從以下方向擴展:

  1. 動態SQL支持:實現if/where/foreach等動態SQL標籤的解析,增強SQL靈活性;
  2. 連接池集成:集成HikariCP連接池,優化連接管理,提升性能;
  3. 高級結果映射:支持下劃線轉駝峯、一對一/一對多關聯查詢映射;
  4. 緩存實現:添加一級緩存和二級緩存,減少數據庫查詢次數;
  5. 事務優化:實現完整的事務管理器,支持事務隔離級別和傳播行為;
  6. 註解驅動:支持通過註解配置SQL,無需編寫Mapper.xml;
  7. 插件機制:提供插件擴展點,支持自定義攔截器(如日誌增強、性能監控等)。

七、總結與面試考點梳理

7.1 總結

本文從0到1手寫實現了一套簡易但完整的MyBatis框架,涵蓋了MyBatis的核心組件(配置解析、數據源、執行器、Mapper代理、會話管理)和核心流程(配置加載→會話創建→代理生成→SQL執行→結果映射)。通過手寫實現,我們深入理解了MyBatis的底層原理:

  • 配置解析本質是XML解析+對象封裝,將配置信息存入核心配置容器;
  • Mapper代理的核心是JDK動態代理,將接口方法調用轉化為SQL執行;
  • SQL執行的核心是封裝JDBC操作,屏蔽底層細節;
  • 結果映射的核心是反射機制,實現ResultSet到Java對象的自動轉化。

掌握這些底層原理,不僅能讓我們更靈活地使用MyBatis進行開發,還能快速定位和解決開發中遇到的框架相關問題。

7.2 面試考點梳理

手寫MyBatis涉及的核心知識點,也是面試中高頻考察的考點,整理如下:

  1. MyBatis的核心組件有哪些?各自的作用是什麼?

    • 答:核心組件包括Configuration(配置容器)、SqlSessionFactory(會話工廠)、SqlSession(會話)、Executor(執行器)、MapperProxy(Mapper代理)、MapperStatement(Mapper映射信息)等。作用參考本文2.2節核心架構設計。
  2. MyBatis的Mapper代理機制原理是什麼?為什麼Mapper接口不需要實現類?

    • 答:底層基於JDK動態代理,通過MapperProxyFactory創建MapperProxy,再通過Proxy.newProxyInstance生成代理對象。調用Mapper接口方法時,會被MapperProxy的invoke()方法攔截,轉化為SQL執行,因此不需要手動編寫實現類。
  3. MyBatis的SQL執行流程是什麼?

    • 答:加載配置文件→解析生成Configuration→創建SqlSessionFactory→獲取SqlSession→獲取Mapper代理對象→調用接口方法→代理對象攔截並獲取MapperStatement→Executor執行SQL(獲取連接、綁定參數、執行SQL)→結果映射→返回結果。
  4. MyBatis的結果映射原理是什麼?

    • 答:通過反射機制,加載結果類型Class,獲取ResultSet元數據(列名、列數),遍歷ResultSet每一行數據,創建實體類對象,通過字段名反射賦值,最終將實體類對象列表返回。
  5. MyBatis與JDBC的區別是什麼?

    • 答:①MyBatis封裝了JDBC的冗餘代碼(如獲取連接、預處理、關閉資源等);②支持XML/註解配置SQL,靈活易用;③提供Mapper代理機制,無需編寫實現類;④支持結果自動映射,無需手動封裝結果集;⑤支持動態SQL、緩存等高級特性。
  6. 什麼是動態SQL?MyBatis是如何實現動態SQL的?

    • 答:動態SQL是指根據參數條件動態拼接SQL語句。官方MyBatis通過XML標籤(if/where/foreach等)和OGNL表達式解析,在解析Mapper.xml時動態生成SQL語句。本文手寫版本未實現,可通過擴展XML解析邏輯實現。
  7. MyBatis的緩存機制是什麼?一級緩存和二級緩存的區別?

    • 答:MyBatis通過緩存減少數據庫查詢次數,提升性能。一級緩存是SqlSession級別,默認開啓,緩存範圍是當前會話;二級緩存是Mapper級別,需要手動開啓,緩存範圍是同一個Mapper接口的所有會話。本文手寫版本未實現,可通過在SqlSession或Mapper層面添加緩存容器(如HashMap)實現。

八、附錄:完整項目代碼結構(最終版)

com.jam.demo
├── mybatis
│   ├── config          # 配置相關
│   │   ├── Configuration.java
│   │   ├── XmlConfigBuilder.java
│   │   └── XmlMapperBuilder.java
│   ├── session         # 會話相關
│   │   ├── SqlSession.java
│   │   ├── SqlSessionFactory.java
│   │   ├── DefaultSqlSession.java
│   │   ├── DefaultSqlSessionFactory.java
│   │   └── SqlSessionFactoryBuilder.java
│   ├── executor        # 執行器相關
│   │   ├── Executor.java
│   │   └── SimpleExecutor.java
│   ├── mapping         # 映射相關
│   │   └── MapperStatement.java
│   ├── proxy           # Mapper代理相關
│   │   ├── MapperProxy.java
│   │   └── MapperProxyFactory.java
│   └── datasource      # 數據源相關
│       ├── DataSource.java
│       └── SimpleDataSource.java
├── mapper              # Mapper接口
│   └── UserMapper.java
├── pojo                # 實體類
│   └── User.java
├── test                # 測試類
│   └── HandwriteMyBatisTest.java
└── resources           # 配置文件
    └── config
        ├── mybatis-config.xml
        └── UserMapper.xml

九、使用説明與注意事項

9.1 項目使用步驟

  1. 克隆/下載項目代碼,導入IDE;
  2. 執行MySQL腳本創建數據庫和表;
  3. 修改mybatis-config.xml中的數據庫連接信息,適配本地環境;
  4. 執行mvn clean install構建項目;
  5. 運行HandwriteMyBatisTest類中的測試方法,驗證功能;
  6. 擴展開發:可基於現有代碼擴展動態SQL、連接池、緩存等功能。

9.2 注意事項

  1. 本文代碼基於JDK 17編寫,低於17的JDK版本可能存在語法兼容問題;
  2. 數據庫版本為MySQL 8.0,使用低版本MySQL時,需修改驅動類名(如MySQL 5.x驅動類名為com.mysql.jdbc.Driver)和連接URL參數;
  3. 手寫版本為簡化實現,僅適用於學習和理解原理,不建議直接用於生產環境;
  4. 擴展功能時,需遵循MyBatis的核心設計思想,保持組件職責單一,確保代碼可維護性。

本文由mdnice多平台發佈

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.