一、引言
在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的核心設計思想,保持組件職責單一,確保代碼可維護性。
本文由mdnice多平台發佈