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>

配置数据库连接(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日志

2. 核心组件使用

(1)实体类(Entity)
java 复制代码
public class User {
    private Long id;
    private String userName;  // 对应数据库 user_name 字段(驼峰转换)
    private Integer age;
    
    // 省略 getter/setter
}
(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);
}
(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>
(4)Service 层调用
java 复制代码
@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);
    }
}

3. 关键特性应用

  • 分页查询 :结合 PageHelper 插件

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

    xml 复制代码
    <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>

二、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>

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

3. 缓存机制

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

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

开启二级缓存

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

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

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 的代理,确保线程安全。

java 复制代码
// 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);
        }
    }
}
(2)Mapper 接口代理

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

java 复制代码
// 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()));
        }
    });
}
(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);
    }
}
(4)结果映射

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

java 复制代码
// 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);
}

3. 动态 SQL 解析原理

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

java 复制代码
// 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);
}

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

java 复制代码
@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;
}

4. 一级缓存实现原理

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

java 复制代码
// 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;
}

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

  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 解析、缓存机制等。

参考资料


Studying will never be ending.

▲如有纰漏,烦请指正~~

相关推荐
普通网友4 小时前
【Spring Boot】Spring Boot解决循环依赖
spring boot·tomcat
皮皮林5515 小时前
SpringBoot + FFmpeg + ZLMediaKit 实现本地视频推流
spring boot
千码君20166 小时前
Go语言:解决 “package xxx is not in std”的思路
开发语言·后端·golang
rexling16 小时前
【Spring Boot】Spring Boot解决循环依赖
java·前端·spring boot
兜兜风d'6 小时前
RabbitMQ消息分发详解:从默认轮询到智能负载均衡
spring boot·分布式·rabbitmq·负载均衡·ruby·java-rabbitmq
咖啡教室6 小时前
每日一个计算机小知识:DHCP
后端·网络协议
咖啡教室7 小时前
每日一个计算机小知识:ARP协议
后端·网络协议
CS Beginner7 小时前
【搭建】个人博客网站的搭建
java·前端·学习·servlet·log4j·mybatis
李慕婉学姐7 小时前
Springboot旅游管理系统8cx8xy5m(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·旅游