MyBatis 核心体系:核心接口、动态 SQL 原理、插件底层机制

MyBatis 是国内企业项目主流半自动 ORM 持久层框架。很多开发只会写 Mapper XML,但是对底层组件调用链路、动态 SQL 生成原理、MyBatis 插件拦截机制理解很浅。本文从 JDBC 痛点入手,完整讲解 MyBatis 核心接口与组件、动态 SQL 底层 SqlNode+OGNL 原理、四大拦截对象插件机制,附带大量实战示例、踩坑总结,夯实 MyBatis 底层基础,为 MyBatis-Plus 学习做好铺垫。

前言

原生 JDBC 开发存在大量重复样板代码:加载驱动、创建数据库连接、创建 PreparedStatement、手动设置参数、遍历 ResultSet 封装实体,最后手动关闭资源。一旦忘记关闭连接,极易造成数据库连接泄漏。

MyBatis 对 JDBC 做轻量级封装,属于半自动 ORM:SQL 由开发人员编写,框架负责参数绑定、结果集映射。对比 Hibernate 全自动 ORM,MyBatis SQL 完全可控,方便 SQL 优化,因此在国内后端项目广泛使用。

MyBatis 没有脱离 JDBC,所有数据库操作最终仍然由 JDBC 完成。

一、MyBatis 核心接口与组件全解析

整体调用链路

SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → Mapper 代理对象 → MappedStatement → Executor → StatementHandler → JDBC

1、SqlSessionFactoryBuilder 构建器,一次性对象。读取 XML/Java 注解配置,解析全部配置信息,构建SqlSessionFactory。构建完成即可丢弃,不需要常驻内存。

2、SqlSessionFactory 全局单例,一个数据库对应一个 SqlSessionFactory ,项目启动时创建,项目销毁释放。作用是生产 SqlSession 会话。Spring 整合 MyBatis 场景,由SqlSessionFactoryBean创建。

3、SqlSession 代表一次数据库会话,线程不安全,禁止多线程共用,使用完毕必须关闭。 两大能力:

  • 直接调用selectOne、selectList、insert、update、delete执行 SQL;
  • getMapper(Class<T> mapper)获取 Mapper 接口代理对象。

4、MapperProxy(Mapper 动态代理) Mapper 接口不需要写实现类 。sqlSession.getMapper()底层使用JDK 动态代理 生成MapperProxy代理对象。 调用 Mapper 接口任意方法,进入MapperProxy#invoke():

  • 封装MapperMethod对象;
  • 根据方法签名找到对应的MappedStatement;
  • 判断 CRUD 类型,调用 SqlSession 执行 SQL。

5、MappedStatement

项目启动阶段,解析 Mapper XML 或者@Select/@Insert注解,每一条 SQL 语句生成一个 MappedStatement 对象,存入全局 Configuration。

存储信息:SQL 模板、入参类型、返回类型、缓存配置、主键策略、SQL 操作类型。

6、Configuration MyBatis 全局配置类,全局唯一。存放所有 MappedStatement、类型别名、TypeHandler、插件、驼峰命名转换、日志等全部配置。

7、Executor 执行器 SqlSession 底层调度核心,管理一级缓存,调用 StatementHandler。3 种类型:

  • SIMPLE 默认:每次新建 Statement;
  • REUSE:复用 PreparedStatement;
  • BATCH:批量攒 SQL,统一提交,适合大批量插入提升性能。

8、StatementHandler、ParameterHandler、ResultSetHandler 这三个和 Executor 合称MyBatis 四大可拦截对象,也是插件机制的核心。

  • StatementHandler:SQL 语句处理器,预编译 SQL、设置参数,改写 SQL 的插件都拦截这个对象;
  • ParameterHandler:入参参数处理;
  • ResultSetHandler:JDBC 结果集封装,映射为 Java 实体。

9、TypeHandler Java 类型 ↔ JDBC 数据库类型转换。内置 String、Integer、Date 等处理器;可自定义 TypeHandler,处理 JSON、加密字段等特殊场景。

MyBatis 完整执行流程

  1. 项目启动,SqlSessionFactoryBuilder 读取配置,构建 SqlSessionFactory;
  2. 通过 SqlSessionFactory 打开会话,创建 SqlSession;
  3. SqlSession 获取 Mapper 接口代理对象 MapperProxy;
  4. 调用 Mapper 方法,触发 MapperProxy#invoke;
  5. 构建 MapperMethod,查找 MappedStatement;
  6. SqlSession 交给 Executor 执行;
  7. Executor 创建 StatementHandler,TypeHandler 完成参数类型转换;
  8. StatementHandler 调用 JDBC PreparedStatement 执行预编译 SQL;
  9. ResultSetHandler 封装 ResultSet,映射 Java 实体;
  10. 会话关闭,释放数据库资源。

二、动态 SQL 标签与底层生成原理(重点)

1. 常用动态 SQL 标签

if、where、trim、choose/when/otherwise、set、foreach。 解决痛点:多条件组合查询,手动拼接 SQL 容易多出多余and/or、逗号,产生 SQL 语法错误。

示例 XML:

XML 复制代码
<select id="selectUserList" resultType="User">
    SELECT id,name,age,status FROM user
    <where>
        <if test="name != null and name != ''">
            AND name like concat('%',#{name},'%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>
  • <where>:只有内部条件成立才生成 WHERE 关键字,自动删除第一个多余 AND/OR;
  • <set>:update 语句使用,自动移除末尾多余逗号;
  • <trim>:自定义前缀、后缀,灵活实现 where/set 能力;
  • <choose>:类似 switch 单选,只匹配一条条件;
  • <foreach>:集合遍历,常用于 IN 查询、批量插入。

2. 动态 SQL 底层原理:SqlNode 语法树 + OGNL 表达式

考点:动态 SQL 不是简单字符串拼接。

  1. 启动阶段 MyBatis 解析 XML,把动态 SQL 片段解析为SqlNode 抽象语法树 。SqlNode 是顶层接口,有多个实现类:IfSqlNode、WhereSqlNode、TrimSqlNode、ForEachSqlNode、TextSqlNode、MixedSqlNode。
  2. 运行阶段 传入参数对象,递归遍历 SqlNode 树:
  • OGNL 表达式引擎解析<if test="xxx">表达式,判断条件是否成立;
  • 条件成立拼接 SQL 片段,不成立跳过;
  • 全部节点遍历完成得到完整 SQL;
  • 将#{}替换为?占位符,生成 PreparedStatement 预编译 SQL;
  • TypeHandler 完成参数绑定。

3. #{} 和 ${} 对比

表格

符号 原理 安全性 适用场景
#{} 预编译占位符,PreparedStatement 防 SQL 注入 普通查询条件,优先推荐
${} 直接字符串拼接,无预编译 存在注入风险 表名、排序字段,禁止接收前端参数

三、MyBatis 插件 Interceptor 机制(MP 插件的底层基础)

MyBatis 提供插件扩展能力,通过Interceptor拦截器,只能拦截四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。 底层原理:JDK 动态代理,对目标对象做多层代理,在方法执行前后做增强。

MyBatis-Plus 分页插件、乐观锁、逻辑删除全部基于 MyBatis 插件实现。

拦截对象用途:

  1. Executor:拦截 CRUD 执行,做日志、缓存增强;
  2. StatementHandler:最常用,拦截 SQL 构建,改写 SQL;
  3. ParameterHandler:拦截参数设置;
  4. ResultSetHandler:拦截结果集封装。

自定义插件开发简要流程:

  1. 实现 Interceptor 接口;
  2. 指定拦截签名,声明要拦截的对象与方法;
  3. 在 MyBatis 配置中注册插件。

注意:插件是层层代理,注册顺序会影响执行顺序。多个插件共存时顺序非常关键。

四、MyBatis 缓存机制

  1. 一级缓存(SqlSession 级别,默认开启) 底层 HashMap,同一个 SqlSession 内,相同 SQL + 参数,查询结果缓存。 触发清空:增删改操作、手动 clearCache、会话关闭。 Spring 整合 MyBatis 时,SqlSessionTemplate 每次调用创建新 SqlSession,一级缓存基本失效,业务不要依赖一级缓存。
  2. 二级缓存(Mapper 命名空间级别,默认关闭) 多个 SqlSession 共享缓存,需要在 Mapper.xml 配置<cache/>,实体实现序列化接口。事务提交后数据写入缓存;增删改清空该 Mapper 全部缓存。

生产环境慎用,多表关联场景容易出现脏读;分布式项目优先 Redis 做分布式缓存。

五、开发踩坑清单

  1. <if test>判断数字类型使用age == '',OGNL 表达式判断失效;
  2. Mapper 多个入参,忘记@Param注解,XML 无法识别参数;
  3. 数据库下划线字段,实体驼峰,未开启mapUnderscoreToCamelCase,查询字段全部 null;
  4. foreach 循环集合为空,未做判空,直接执行 SQL 抛出语法异常;
  5. ${}接收前端传入参数,造成 SQL 注入漏洞;
  6. 插件注册顺序错误,导致 SQL 改写插件失效;
  7. 项目开启二级缓存,多 Mapper 操作同一张表,产生脏读。

六、面试高频问答

Q1:Mapper 接口为什么不需要实现类?底层原理?

A:使用 JDK 动态代理生成 MapperProxy 代理对象,调用 Mapper 方法进入 invoke,封装 MapperMethod,找到 MappedStatement 交给 SqlSession 执行 SQL。

Q2:MyBatis 动态 SQL 底层原理?

A:XML 启动解析为 SqlNode 语法树;运行时通过 OGNL 表达式判断条件,递归拼接 SQL 片段;#{}转为预编译占位符,防止 SQL 注入。

Q3:MyBatis 插件可以拦截哪四大对象?MP 分页插件拦截哪个?

A:Executor、StatementHandler、ParameterHandler、ResultSetHandler。分页插件拦截 StatementHandler,实现 SQL 改写。

Q4:一级缓存和二级缓存区别,Spring 环境一级缓存为什么失效?

A:一级缓存 SqlSession 级别,默认开启;二级缓存 Mapper 命名空间级别,手动开启。Spring 整合 MyBatis 每次操作新建 SqlSession,会话销毁,一级缓存无法复用。

结语

MyBatis 的核心本质是 JDBC 的封装,动态 SQL 依靠 SqlNode 语法树 + OGNL,插件依靠 JDK 动态代理拦截四大对象。吃透 MyBatis 底层组件、动态 SQL、插件机制,才能理解 MyBatis-Plus 的增强原理,看懂 MP 分页、逻辑删除等插件的底层实现。

相关推荐
catino1 小时前
SQL调优
数据库·sql
XiYang-DING1 小时前
P5528.带过期时间的 LRU 缓存
java·开发语言·缓存
SimonKing1 小时前
DeepSeek 新模型免费白嫖,夯夯夯,赶紧用起来
java·后端·程序员
SabrinaW7111 小时前
基于 C++17 与 RTTR 的注册式对象工厂:配置驱动的动态对象创建
java·前端·数据库
zhangx1234_2 小时前
javaEE 多线程4
java·开发语言
大鳥2 小时前
JSON 解析最佳实践:从入门到生产级优化
hive·sql·spark·json
pjj198542 小时前
Spark SQL 核心知识详解:DataFrame、DSL 与数据源
大数据·sql·spark
geovindu2 小时前
sql:SQL Views and Materialized Views using mysql 9.0
大数据·数据库·数据仓库·sql·mysql·数据库开发·数据库架构
中电金信2 小时前
从业务调优到分层缓存:DeepSeek V4 Flash的PPU实践
java·开发语言