MyBatis 源码深度解析:从 Spring Boot 实战到底层原理

MyBatis 源码深度解析:从 Spring Boot 实战到底层原理

作为 Java 生态中最流行的持久层框架之一,MyBatis 以其[灵活的]SQL 控制和极简的配置深受开发者喜爱。本文将从 Spring Boot 中的实际用法出发,逐步深入 MyBatis 核心源码,解析其设计思想与运行机制。

一、Spring Boot 中 MyBatis 的实战用法

在实际开发中,MyBatis 常与 Spring Boot 结合使用,通过 Starter 快速集成。以下是完整的使用流程:

1. 环境搭建

引入依赖(Maven):

xml 复制代码
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

AI写代码xml
12345678910

配置数据库连接(application.yml):

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml  # Mapper XML 文件位置
  type-aliases-package: com.example.entity  # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL日志

AI写代码yaml
12345678910111213

2. 核心组件使用

(1)实体类(Entity)
kotlin 复制代码
public class User {
    private Long id;
    private String userName;  // 对应数据库 user_name 字段(驼峰转换)
    private Integer age;
    
    // 省略 getter/setter
}

AI写代码java
运行
1234567
(2)Mapper 接口
java 复制代码
@Mapper  // 标记为MyBatis映射接口
public interface UserMapper {
    // 注解方式编写SQL
    @Select("SELECT * FROM user WHERE id = #{id}")
    User selectById(Long id);
    
    // XML方式编写SQL(对应UserMapper.xml)
    int insert(User user);
}

AI写代码java
运行
123456789
(3)Mapper XML 文件(resources/mapper/UserMapper.xml)
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">
    <insert id="insert" parameterType="User">
        INSERT INTO user (user_name, age) 
        VALUES (#{userName}, #{age})
    </insert>
</mapper>

AI写代码xml
12345678910
(4)Service 层调用
typescript 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;  // 注入Mapper接口(MyBatis自动生成实现类)
    
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
    
    public void addUser(User user) {
        userMapper.insert(user);
    }
}

AI写代码java
运行
12345678910111213

3. 关键特性应用

  • 分页查询 :结合 PageHelper 插件

    ini 复制代码
    PageHelper.startPage(1, 10);  // 第1页,每页10条
    List<User> users = userMapper.selectAll();
    Page<User> page = (Page<User>) users;  // 分页结果
    
    AI写代码java
    运行
    123
  • 动态 SQL :在 XML 中使用 <if><foreach> 等标签

    sql 复制代码
    <select id="selectByCondition" parameterType="User">
        SELECT * FROM user
        <where>
            <if test="userName != null">AND user_name LIKE CONCAT('%', #{userName}, '%')</if>
            <if test="age != null">AND age = #{age}</if>
        </where>
    </select>
    
    AI写代码xml
    1234567

二、MyBatis 核心特性解析

MyBatis 的强大源于其独特的设计,以下是工作中最常用的核心特性:

1. 动态 SQL

动态 SQL 允许根据参数动态生成 SQL 语句,避免了字符串拼接的繁琐和 SQL 注入风险。核心标签包括:

  • <if>:条件判断
  • <where>/<set>:自动处理多余的 AND/, 符号
  • <foreach>:循环遍历(如批量插入 IN 条件)
  • <sql>/<include>:SQL 片段复用

优势:在多条件查询、动态更新等场景中非常实用,例如后台管理系统的复杂搜索功能。

2. 结果映射(ResultMap)

解决实体类与数据库表字段名不匹配的问题(即使不开启驼峰转换也能通过配置映射):

xml 复制代码
<resultMap id="UserResultMap" type="User">
    <id column="user_id" property="id"/>  <!-- 主键映射 -->
    <result column="user_name" property="userName"/>  <!-- 普通字段映射 -->
    <association property="role" javaType="Role">  <!-- 一对一关联 -->
        <id column="role_id" property="id"/>
        <result column="role_name" property="name"/>
    </association>
</resultMap>

AI写代码xml
12345678

应用场景:多表关联查询(一对一、一对多),复杂对象的映射。

3. 缓存机制

MyBatis 提供两级缓存,减少数据库访问次数:

  • 一级缓存:SqlSession 级别(默认开启),同一 SqlSession 内的查询结果会被缓存。
  • 二级缓存:Mapper 级别(需手动开启),多个 SqlSession 共享缓存。

开启二级缓存

xml 复制代码
<!-- 在Mapper XML中开启 -->
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>

AI写代码xml
12

注意:二级缓存适合查询频繁、修改较少的数据(如字典表),避免缓存一致性问题。

4. 插件机制

MyBatis 允许通过插件拦截四大核心组件的方法,实现自定义功能:

  • Executor:执行器(update、query等方法)
  • ParameterHandler:参数处理器
  • ResultSetHandler:结果集处理器
  • StatementHandler:SQL 语句处理器

典型应用:分页插件(PageHelper)、数据权限控制、SQL 日志打印等。

三、MyBatis 源码深度解析

1. 核心架构概览

MyBatis 核心架构分为三层:

  1. 接口层 :对外提供 API(如 SqlSessionMapper 接口)
  2. 核心处理层:处理 SQL 解析、参数映射、结果映射等核心逻辑
  3. 基础支撑层:提供数据源、事务管理、缓存等基础服务

2. 核心流程:一条 SQL 的执行过程

userMapper.selectById(1L) 为例,解析 [MyBatis 执行流程]:

(1)获取 SqlSession

在 Spring 环境中,SqlSessionSqlSessionTemplate 管理,其本质是对 SqlSession 的代理,确保线程安全。

kotlin 复制代码
// SqlSessionTemplate 中的代理逻辑
private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        SqlSession sqlSession = getSqlSession(
            SqlSessionTemplate.this.sqlSessionFactory,
            SqlSessionTemplate.this.executorType,
            SqlSessionTemplate.this.exceptionTranslator);
        try {
            Object result = method.invoke(sqlSession, args);  // 调用SqlSession方法
            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                sqlSession.commit(true);  // 非事务环境自动提交
            }
            return result;
        } catch (Throwable t) {
            // 异常处理
        } finally {
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
    }
}

AI写代码java
运行
123456789101112131415161718192021
(2)Mapper 接口代理

MyBatis 通过 JDK 动态代理为 UserMapper 生成实现类(MapperProxy),当调用 selectById 时,实际执行 MapperProxy.invoke() 方法。

arduino 复制代码
// MapperProxy 核心代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);  // 处理Object类方法(如toString)
    } else {
        // 执行Mapper方法
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
}

// 缓存Mapper方法执行器
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
            // 处理默认方法
        } else {
            return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
    });
}

AI写代码java
运行
123456789101112131415161718192021
(3)SQL 解析与执行

MapperMethod 将方法调用转换为对 SqlSession 的操作(如 selectOne),最终由 Executor 执行 SQL:

java 复制代码
// SimpleExecutor 执行查询的核心逻辑
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 创建StatementHandler(处理SQL预编译、参数设置)
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 获取数据库连接并创建Statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 执行查询并处理结果
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

AI写代码java
运行
12345678910111213141516
(4)结果映射

ResultSetHandler 将数据库返回的 ResultSet 转换为 Java 对象:

ini 复制代码
// DefaultResultSetHandler 核心逻辑
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    final List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;
    // 获取第一个结果集
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    // 获取结果映射配置
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 处理结果集映射
    while (rsw != null && resultMapCount > resultSetCount) {
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // 将结果集映射为Java对象
        handleResultSet(rsw, resultMap, multipleResults, null);
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
    }
    // 处理嵌套结果集等其他逻辑
    return collapseSingleResultList(multipleResults);
}

AI写代码java
运行
12345678910111213141516171819202122

3. 动态 SQL 解析原理

动态 SQL 的解析发生在 MyBatis 初始化阶段,通过 XMLScriptBuilder 解析 <if><where> 等标签,生成 SqlNode 树,最终在执行时动态拼接 SQL。

ini 复制代码
// XMLScriptBuilder 解析动态SQL
private MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            // 静态文本节点
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
            // 处理动态标签(if、foreach等)
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

AI写代码java
运行
1234567891011121314151617181920212223242526272829

执行时,DynamicSqlSource.getBoundSql() 会根据参数动态生成最终 SQL:

ini 复制代码
@Override
public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 执行SqlNode树,拼接SQL
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 绑定参数
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
}

AI写代码java
运行
12345678910111213

4. 一级缓存实现原理

一级缓存(PerpetualCache)存储在 SqlSession 对应的 Executor 中,默认开启,缓存 key 由 MappedStatement ID + SQL + 参数 + 分页信息 组成。

scss 复制代码
// BaseExecutor 中的查询缓存逻辑
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 创建缓存key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 检查一级缓存
    List<E> list;
    try {
        queryStack++;
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 从缓存中获取结果
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 缓存未命中,查询数据库
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    // 处理二级缓存
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // 清除语句级别的缓存
            clearLocalCache();
        }
    }
    return list;
}

AI写代码java
运行
123456789101112131415161718192021222324252627282930313233343536373839

四、工作中的最佳实践与优化建议

  1. SQL 优化

    • 避免 SELECT *,只查询需要的字段
    • 复杂查询优先使用 XML 方式编写,便于维护
    • 合理使用索引,避免全表扫描
  2. 缓存策略

    • 一级缓存默认开启,但在事务中需注意缓存有效性
    • 二级缓存谨慎使用,建议只在只读表或极少更新的表中开启
    • 分布式环境下,优先使用 Redis 等分布式缓存替代二级缓存
  3. 性能监控

    • 开启 MyBatis 日志,监控 SQL 执行时间
    • 使用 PageHelper 分页时,注意 count 查询的性能
    • 避免 N+1 查询问题(可通过 associationcollectionfetchType="eager" 解决)
  4. 安全规范

    • 禁止直接拼接 SQL 字符串,使用 #{} 占位符(防止 SQL 注入)
    • 敏感字段加密存储,查询时通过插件解密

五、总结

MyBatis 以其灵活的设计和强大的功能,成为 Java 持久层框架的佼佼者。通过本文的分析,我们从 Spring Boot 实战出发,深入理解了 MyBatis 的核心特性和源码实现,包括 SQL 执行流程、动态 SQL 解析、缓存机制等。

相关推荐
刘晓飞1 分钟前
nestjs 中的 rxjs
后端
编码忘我1 小时前
java策略模式实战之优惠券
java·后端
anzhxu1 小时前
SpringBoot 3.x 整合swagger
java·spring boot·后端
青椒啊1 小时前
DPDK入门到精通(一)
后端
小江的记录本1 小时前
【Bean】JavaBean(原生规范)/ Spring Bean 【重点】/ 企业级Bean(EJB/Jakarta Bean)
java·数据库·spring boot·后端·spring·spring cloud·mybatis
中国胖子风清扬1 小时前
Camunda 8 概念详解:梳理新一代工作流引擎的核心概念与组件
java·spring boot·后端·spring cloud·ai·云原生·spring webflux
前端付豪1 小时前
实现必要的流式输出(Streaming)
前端·后端·agent
go4it1 小时前
Java26的新特性
后端
木易 士心1 小时前
深入理解 MySQL 权限撤销(REVOKE)机制:从语法到安全实践
数据库·后端·mysql·安全
yhole1 小时前
Spring Boot整合Redisson的两种方式
java·spring boot·后端