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 完整执行流程
- 项目启动,SqlSessionFactoryBuilder 读取配置,构建 SqlSessionFactory;
- 通过 SqlSessionFactory 打开会话,创建 SqlSession;
- SqlSession 获取 Mapper 接口代理对象 MapperProxy;
- 调用 Mapper 方法,触发 MapperProxy#invoke;
- 构建 MapperMethod,查找 MappedStatement;
- SqlSession 交给 Executor 执行;
- Executor 创建 StatementHandler,TypeHandler 完成参数类型转换;
- StatementHandler 调用 JDBC PreparedStatement 执行预编译 SQL;
- ResultSetHandler 封装 ResultSet,映射 Java 实体;
- 会话关闭,释放数据库资源。
二、动态 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 不是简单字符串拼接。
- 启动阶段 MyBatis 解析 XML,把动态 SQL 片段解析为SqlNode 抽象语法树 。SqlNode 是顶层接口,有多个实现类:
IfSqlNode、WhereSqlNode、TrimSqlNode、ForEachSqlNode、TextSqlNode、MixedSqlNode。 - 运行阶段 传入参数对象,递归遍历 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 插件实现。
拦截对象用途:
- Executor:拦截 CRUD 执行,做日志、缓存增强;
- StatementHandler:最常用,拦截 SQL 构建,改写 SQL;
- ParameterHandler:拦截参数设置;
- ResultSetHandler:拦截结果集封装。
自定义插件开发简要流程:
- 实现 Interceptor 接口;
- 指定拦截签名,声明要拦截的对象与方法;
- 在 MyBatis 配置中注册插件。
注意:插件是层层代理,注册顺序会影响执行顺序。多个插件共存时顺序非常关键。
四、MyBatis 缓存机制
- 一级缓存(SqlSession 级别,默认开启) 底层 HashMap,同一个 SqlSession 内,相同 SQL + 参数,查询结果缓存。 触发清空:增删改操作、手动 clearCache、会话关闭。 Spring 整合 MyBatis 时,SqlSessionTemplate 每次调用创建新 SqlSession,一级缓存基本失效,业务不要依赖一级缓存。
- 二级缓存(Mapper 命名空间级别,默认关闭) 多个 SqlSession 共享缓存,需要在 Mapper.xml 配置
<cache/>,实体实现序列化接口。事务提交后数据写入缓存;增删改清空该 Mapper 全部缓存。
生产环境慎用,多表关联场景容易出现脏读;分布式项目优先 Redis 做分布式缓存。
五、开发踩坑清单
<if test>判断数字类型使用age == '',OGNL 表达式判断失效;- Mapper 多个入参,忘记
@Param注解,XML 无法识别参数; - 数据库下划线字段,实体驼峰,未开启
mapUnderscoreToCamelCase,查询字段全部 null; - foreach 循环集合为空,未做判空,直接执行 SQL 抛出语法异常;
${}接收前端传入参数,造成 SQL 注入漏洞;- 插件注册顺序错误,导致 SQL 改写插件失效;
- 项目开启二级缓存,多 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 分页、逻辑删除等插件的底层实现。