一、引言
在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对象与数据库表的映射”,具体拆解为以下需求:
- 配置解析:加载mybatis-config.xml核心配置(数据源、Mapper映射路径等)和Mapper.xml映射配置(SQL语句、参数映射、结果映射等);
- Mapper代理:通过动态代理机制,让开发者直接调用Mapper接口方法即可执行对应SQL,无需编写接口实现类;
- SQL执行:封装JDBC操作,完成SQL参数绑定、语句执行;
- 结果映射:将JDBC查询返回的ResultSet结果集,自动映射为Java实体类对象;
- 会话管理:提供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&serverTimezone=UTC&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 测试执行步骤
- 执行MySQL脚本创建
handwrite_mybatis数据库和user表; - 在IDE中打开
HandwriteMyBatisTest类,执行testCrud()方法; - 观察控制台日志和数据库数据变化,验证CRUD功能是否正常。
5.4.3 预期测试结果
- 控制台日志输出“新增用户成功”“查询用户成功”“更新用户成功”“删除用户成功”等信息,无异常抛出;
- 数据库中先新增一条用户数据,更新后数据字段变化,删除后数据不存在;
- 单元测试断言全部通过,无失败用例。
5.4.4 常见问题排查
- 数据库连接失败:检查MySQL服务是否启动,
mybatis-config.xml中的URL、用户名、密码是否正确; - 配置文件找不到:确保
mybatis-config.xml和UserMapper.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对象(Configuration、MapperStatement),核心流程如下:
XmlConfigBuilder解析mybatis-config.xml,先解析数据源配置,创建SimpleDataSource存入Configuration;- 再解析
mappers节点,加载对应的Mapper.xml文件,交给XmlMapperBuilder解析; XmlMapperBuilder解析Mapper.xml的namespace(对应Mapper接口全类名)和SQL标签(select/insert/update/delete);- 将每个SQL标签的信息封装为
MapperStatement,以namespace+id为key存入Configuration的mapperStatementMap中; - 后续SQL执行时,通过
namespace+methodName即可快速获取对应的MapperStatement,拿到SQL语句和参数/结果配置。
6.3 SQL执行与结果映射原理
6.3.1 SQL执行流程
SQL执行的核心是Executor(执行器),它封装了JDBC的全套操作,核心流程:
- 从
Configuration中获取数据源,通过数据源获取数据库连接; - 处理原始SQL,将
#{}占位符替换为?,生成可预处理的SQL语句; - 创建
PreparedStatement,通过反射获取方法参数值,绑定到?占位符上; - 执行SQL(查询执行
executeQuery(),增删改执行executeUpdate()); - 关闭连接等资源(简化实现,实际MyBatis会用连接池管理连接)。
6.3.2 结果映射原理
结果映射的核心是将ResultSet转化为Java实体类对象,核心流程:
- 从
MapperStatement中获取resultType(结果类型全类名),通过Class.forName()加载对应的实体类Class; - 获取
ResultSet的元数据(ResultSetMetaData),得到查询结果的列名和列数; - 遍历
ResultSet,每一行数据对应一个实体类对象,通过反射创建实体类实例; - 遍历查询列,通过列名获取实体类对应的属性,调用
Field.set()方法给属性赋值; - 将所有实体类对象存入列表,返回给调用方。
6.4 与官方MyBatis的差异与扩展方向
6.4.1 与官方MyBatis的核心差异
本文实现的手写MyBatis是简化版,与官方MyBatis的核心差异如下:
- 数据源:手写版本使用简单的JDBC连接,官方版本支持连接池(如Druid、HikariCP)、数据源工厂等;
- SQL解析:手写版本仅支持简单的
#{}占位符替换,官方版本支持复杂的动态SQL(if/where/foreach等)、OGNL表达式解析; - 结果映射:手写版本仅支持属性名与列名一致的映射,官方版本支持下划线转驼峰、复杂结果映射(一对一、一对多)、resultMap高级配置等;
- 事务管理:手写版本的事务提交/回滚是简化实现,官方版本支持完整的事务管理器(JDBC事务、MANAGED事务)、事务隔离级别配置;
- 缓存机制:手写版本未实现缓存,官方版本支持一级缓存(SqlSession级别)、二级缓存(Mapper级别);
- 插件机制:手写版本未实现插件扩展,官方版本支持插件机制,可拦截Executor、StatementHandler等组件;
- 注解支持:手写版本仅支持XML配置SQL,官方版本支持
@Select、@Insert等注解配置SQL。
6.4.2 扩展方向(进阶优化)
如果想进一步完善手写MyBatis,可从以下方向扩展:
- 动态SQL支持:实现if/where/foreach等动态SQL标签的解析,增强SQL灵活性;
- 连接池集成:集成HikariCP连接池,优化连接管理,提升性能;
- 高级结果映射:支持下划线转驼峰、一对一/一对多关联查询映射;
- 缓存实现:添加一级缓存和二级缓存,减少数据库查询次数;
- 事务优化:实现完整的事务管理器,支持事务隔离级别和传播行为;
- 注解驱动:支持通过注解配置SQL,无需编写Mapper.xml;
- 插件机制:提供插件扩展点,支持自定义拦截器(如日志增强、性能监控等)。
七、总结与面试考点梳理
7.1 总结
本文从0到1手写实现了一套简易但完整的MyBatis框架,涵盖了MyBatis的核心组件(配置解析、数据源、执行器、Mapper代理、会话管理)和核心流程(配置加载→会话创建→代理生成→SQL执行→结果映射)。通过手写实现,我们深入理解了MyBatis的底层原理:
- 配置解析本质是XML解析+对象封装,将配置信息存入核心配置容器;
- Mapper代理的核心是JDK动态代理,将接口方法调用转化为SQL执行;
- SQL执行的核心是封装JDBC操作,屏蔽底层细节;
- 结果映射的核心是反射机制,实现ResultSet到Java对象的自动转化。
掌握这些底层原理,不仅能让我们更灵活地使用MyBatis进行开发,还能快速定位和解决开发中遇到的框架相关问题。
7.2 面试考点梳理
手写MyBatis涉及的核心知识点,也是面试中高频考察的考点,整理如下:
- MyBatis的核心组件有哪些?各自的作用是什么?
- 答:核心组件包括Configuration(配置容器)、SqlSessionFactory(会话工厂)、SqlSession(会话)、Executor(执行器)、MapperProxy(Mapper代理)、MapperStatement(Mapper映射信息)等。作用参考本文2.2节核心架构设计。
- MyBatis的Mapper代理机制原理是什么?为什么Mapper接口不需要实现类?
- 答:底层基于JDK动态代理,通过MapperProxyFactory创建MapperProxy,再通过Proxy.newProxyInstance生成代理对象。调用Mapper接口方法时,会被MapperProxy的invoke()方法拦截,转化为SQL执行,因此不需要手动编写实现类。
- MyBatis的SQL执行流程是什么?
- 答:加载配置文件→解析生成Configuration→创建SqlSessionFactory→获取SqlSession→获取Mapper代理对象→调用接口方法→代理对象拦截并获取MapperStatement→Executor执行SQL(获取连接、绑定参数、执行SQL)→结果映射→返回结果。
- MyBatis的结果映射原理是什么?
- 答:通过反射机制,加载结果类型Class,获取ResultSet元数据(列名、列数),遍历ResultSet每一行数据,创建实体类对象,通过字段名反射赋值,最终将实体类对象列表返回。
- MyBatis与JDBC的区别是什么?
- 答:①MyBatis封装了JDBC的冗余代码(如获取连接、预处理、关闭资源等);②支持XML/注解配置SQL,灵活易用;③提供Mapper代理机制,无需编写实现类;④支持结果自动映射,无需手动封装结果集;⑤支持动态SQL、缓存等高级特性。
- 什么是动态SQL?MyBatis是如何实现动态SQL的?
- 答:动态SQL是指根据参数条件动态拼接SQL语句。官方MyBatis通过XML标签(if/where/foreach等)和OGNL表达式解析,在解析Mapper.xml时动态生成SQL语句。本文手写版本未实现,可通过扩展XML解析逻辑实现。
- 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 项目使用步骤
- 克隆/下载项目代码,导入IDE;
- 执行MySQL脚本创建数据库和表;
- 修改
mybatis-config.xml中的数据库连接信息,适配本地环境; - 执行
mvn clean install构建项目; - 运行
HandwriteMyBatisTest类中的测试方法,验证功能; - 扩展开发:可基于现有代码扩展动态SQL、连接池、缓存等功能。
9.2 注意事项
- 本文代码基于JDK 17编写,低于17的JDK版本可能存在语法兼容问题;
- 数据库版本为MySQL 8.0,使用低版本MySQL时,需修改驱动类名(如MySQL 5.x驱动类名为
com.mysql.jdbc.Driver)和连接URL参数; - 手写版本为简化实现,仅适用于学习和理解原理,不建议直接用于生产环境;
- 扩展功能时,需遵循MyBatis的核心设计思想,保持组件职责单一,确保代码可维护性。