从 0 到 1 手写实现 MyBatis 框架:吃透 ORM 底层原理,面试不再慌

简介: 本文带你从0到1手写实现一个简易但完整的MyBatis框架,深入剖析其核心原理,涵盖配置解析、Mapper代理、SQL执行、结果映射等关键流程,助你掌握底层机制,提升开发与面试竞争力。

一、引言

在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节核心架构设计。
  1. MyBatis的Mapper代理机制原理是什么?为什么Mapper接口不需要实现类?
  • 答:底层基于JDK动态代理,通过MapperProxyFactory创建MapperProxy,再通过Proxy.newProxyInstance生成代理对象。调用Mapper接口方法时,会被MapperProxy的invoke()方法拦截,转化为SQL执行,因此不需要手动编写实现类。
  1. MyBatis的SQL执行流程是什么?
  • 答:加载配置文件→解析生成Configuration→创建SqlSessionFactory→获取SqlSession→获取Mapper代理对象→调用接口方法→代理对象拦截并获取MapperStatement→Executor执行SQL(获取连接、绑定参数、执行SQL)→结果映射→返回结果。
  1. MyBatis的结果映射原理是什么?
  • 答:通过反射机制,加载结果类型Class,获取ResultSet元数据(列名、列数),遍历ResultSet每一行数据,创建实体类对象,通过字段名反射赋值,最终将实体类对象列表返回。
  1. MyBatis与JDBC的区别是什么?
  • 答:①MyBatis封装了JDBC的冗余代码(如获取连接、预处理、关闭资源等);②支持XML/注解配置SQL,灵活易用;③提供Mapper代理机制,无需编写实现类;④支持结果自动映射,无需手动封装结果集;⑤支持动态SQL、缓存等高级特性。
  1. 什么是动态SQL?MyBatis是如何实现动态SQL的?
  • 答:动态SQL是指根据参数条件动态拼接SQL语句。官方MyBatis通过XML标签(if/where/foreach等)和OGNL表达式解析,在解析Mapper.xml时动态生成SQL语句。本文手写版本未实现,可通过扩展XML解析逻辑实现。
  1. 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的核心设计思想,保持组件职责单一,确保代码可维护性。
目录
相关文章
|
1月前
|
运维 Java Serverless
Serverless 架构模式深度解析
Serverless并非“无服务器”,而是开发者无需管理服务器,专注业务逻辑。具备按需付费、弹性伸缩、事件驱动等优势,适用于突发流量、定时任务等场景,结合FaaS与BaaS可构建高效应用,是云原生发展的重要方向。
297 1
|
2月前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
1598 89
大厂CIO独家分享:AI如何重塑开发者未来十年
|
设计模式 监控 安全
JUC第一讲:Java并发知识体系详解 + 面试题汇总(P6熟练 P7精通)
JUC第一讲:Java并发知识体系详解 + 面试题汇总(P6熟练 P7精通)
4031 0
|
2月前
|
XML Java 开发者
springboot自动装配的基本原理
Spring Boot自动装配基于“约定大于配置”理念,通过@SpringBootApplication、@EnableAutoConfiguration与spring.factories机制,结合条件注解实现智能Bean加载。它根据依赖自动配置组件,大幅简化开发。其核心是AutoConfigurationImportSelector筛选符合条件的配置类,实现按需装配。开发者可专注业务,享受“开箱即用”的便捷体验。(238字)
|
3月前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
5066 75
|
30天前
|
前端开发 Java API
吃透 Spring 体系结构
本文深入剖析Spring框架的体系结构,围绕IOC(控制反转)和AOP(面向切面编程)两大核心思想,系统讲解了核心容器、AOP、数据访问与Web模块的原理及实战应用,并辨析了常见技术点差异,助力开发者掌握其设计精髓。
89 2
|
24天前
|
SQL 关系型数据库 MySQL
【SQL优化】不再抓瞎!手把手教你读懂MySQL Explain执行计划
本文详解MySQL执行计划工具EXPLAIN,教你读懂其输出的“天书”表格。重点掌握四个核心指标:`type`(访问类型)、`key`(实际使用索引)、`Extra`(额外信息)和`rows`(扫描行数)。通过实战案例解析慢查询成因与优化方案,助你快速定位SQL性能瓶颈,写出高效数据库查询。
|
1月前
|
人工智能 自然语言处理 搜索推荐
2025年中国数字人企业厂商排名与新推荐及新范式
AI数字人融合语音识别、自然语言理解与多模态交互,正重塑虚拟服务新范式。从像衍科技的技术深耕到阿里、百度的场景落地,数字人已广泛应用于内容创作、医疗教育等领域,兼具拟真形象与智能内核。在伦理与技术平衡中,迈向个性化、普惠化的虚实共生未来。
|
5月前
|
传感器 安全 机器人
5G+远程手术:当“低延迟”成为一把手术刀
5G+远程手术:当“低延迟”成为一把手术刀
241 3
|
存储 自然语言处理 API
Transformers 4.37 中文文档(十三)(1)
Transformers 4.37 中文文档(十三)
260 1