【MyBatis-Plus】源码级吃透 MyBatis 与 MyBatis-Plus:两阶段架构、插件机制与底层增强全解析
作者:大家好,我是 CodeStats。一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。
我的技术信条:所有高深的技术,最后都能用大白话讲清楚。如果讲不清楚,说明还没真正理解。
📖 本文收获
✅ 理解 MyBatis 如何封装 JDBC,通过
SqlSession完成数据库操作✅ 掌握 Spring Boot 自动配置 MyBatis 的底层原理
✅ 深入 MyBatis两阶段设计:初始化阶段 + 运行时阶段
✅ 系统学习 MyBatis插件机制,理解分页实现原理
✅ 掌握 MyBatis-Plus 的两阶段增强:
MapperRegistry、DefaultSqlInjector、MybatisPlusInterceptor✅ 理清 MP 插件配置与 MyBatis 原生配置的本质区别
📑 目录
MyBatis 如何封装 JDBC 通过 SqlSession 执行?
Spring Boot 如何自动配置 SqlSessionFactory?
MyBatis 两阶段设计的核心组件与执行步骤
MyBatis 插件机制与分页实现原理
MyBatis-Plus 的两阶段增强机制
MP 插件配置与 MyBatis 原生配置的区别
1. MyBatis 如何封装 JDBC 通过 SqlSession 执行?
原生 JDBC 的痛点
java
// 原生 JDBC 七步走 Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(url, username, password); PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?"); ps.setInt(1, 1); ResultSet rs = ps.executeQuery(); while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); } rs.close(); ps.close(); conn.close();痛点:硬编码、重复代码、资源管理繁琐、结果映射手动。
MyBatis 的核心封装思路
MyBatis 的本质是对 JDBC 的封装,核心解决四个问题:
安全执行 SQL,抽象掉 JDBC 的复杂性
将 Java 参数对象映射到 JDBC PreparedStatement
将 JDBC ResultSet 的行映射为 Java 对象
动态生成 SQL(XML 标签或注解)
核心组件体系
| 组件 | 职责 |
|---|---|
| SqlSessionFactory | 创建 SqlSession(工厂模式) |
| SqlSession | 对外 API,定义 CRUD 方法(门面模式) |
| Executor | SQL 执行器,封装 JDBC 操作 |
| StatementHandler | 处理 Statement,执行 SQL |
| ParameterHandler | 设置 SQL 参数 |
| ResultSetHandler | 将 ResultSet 映射为 Java 对象 |
SqlSession 执行流程(源码级)
Step 1:获取 SqlSession,创建 Executor
java
// DefaultSqlSessionFactory 源码(简化) public SqlSession openSession() { Executor executor = new SimpleExecutor(transaction, autoCommit); // 关键:通过拦截器链生成代理(插件植入点) executor = (Executor) interceptorChain.pluginAll(executor); return new DefaultSqlSession(configuration, executor, autoCommit); }Step 2:获取 Mapper 代理对象
java
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
通过MapperRegistry和JDK 动态代理生成MapperProxy。
Step 3:完整调用链
text
MapperProxy.invoke() → MapperMethod.execute() → SqlSession.selectOne() → 从 Configuration 获取 MappedStatement → Executor.query() → StatementHandler.prepare() → ParameterHandler.setParameters() → Statement.executeQuery() → ResultSetHandler.handleResultSets()
🎯 核心观点
MyBatis 通过SqlSession(门面)+ Executor(执行器)+ StatementHandler(处理器)+ ResultSetHandler(映射器)四层结构,将 JDBC 的复杂性完全封装。开发者只需定义 Mapper 接口和 SQL,框架自动完成参数绑定、SQL 执行和结果映射。
2. Spring Boot 如何自动配置 SqlSessionFactory?
自动配置入口:MybatisAutoConfiguration
Spring Boot 基于META-INF/spring.factories加载MybatisAutoConfiguration:
java
@Configuration @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) @ConditionalOnSingleCandidate(DataSource.class) public class MybatisAutoConfiguration { // ... }关键条件注解:
@ConditionalOnClass:类路径存在 MyBatis 相关类时才生效@ConditionalOnSingleCandidate(DataSource.class):存在 DataSource Bean 时才生效
SqlSessionFactory 创建流程
Step 1:读取application.yml中的 MyBatis 配置项(mapper-locations、config-location等)
Step 2:通过SqlSessionFactoryBean构建(实现了FactoryBean+InitializingBean)
java
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setMapperLocations(this.properties.getMapperLocations()); return factory.getObject(); }Step 3:buildSqlSessionFactory()内部创建Configuration,解析 XML、注册类型处理器、插件等
SqlSessionTemplate 与 Mapper 扫描
SqlSessionTemplate:线程安全,与 Spring 事务管理无缝集成
@MapperScan:通过
MapperScannerConfigurer将 Mapper 接口注册为MapperFactoryBean
🎯 核心观点
Spring Boot 自动配置 MyBatis 的核心流程:
spring.factories→MybatisAutoConfiguration→SqlSessionFactoryBean构建SqlSessionFactory→@MapperScan注册 Mapper。开发者只需添加依赖 + 配置数据源,即可无缝使用 MyBatis。
3. MyBatis 两阶段设计的核心组件与执行步骤
MyBatis 的架构清晰划分为两个阶段:初始化阶段和运行时阶段。
3.1 阶段一:初始化阶段(启动时)
目标:解析配置,构建Configuration对象,注册MappedStatement。
| 核心组件 | 职责 |
|---|---|
SqlSessionFactoryBuilder | 解析 XML/注解,构建 SqlSessionFactory |
Configuration | 全局配置中心,存放所有 MappedStatement |
XMLMapperBuilder | 解析 Mapper XML 文件 |
MapperAnnotationBuilder | 解析 Mapper 接口上的注解 |
MapperRegistry | 注册 Mapper 接口与 MapperProxyFactory 的映射 |
执行步骤:
text
1. 读取 mybatis-config.xml 2. 解析 <environments>、<typeAliases>、<plugins> 等配置 3. 扫描 mapper-locations,解析每个 Mapper XML 文件 → XMLMapperBuilder 解析 <select>、<insert> 等标签 → 生成 MappedStatement 并注册到 Configuration.mappedStatements 4. 扫描 Mapper 接口上的 @Select、@Insert 等注解 → MapperAnnotationBuilder 解析并注册 5. 构建 SqlSessionFactory(DefaultSqlSessionFactory)
关键数据结构:
java
protected final Map<String, MappedStatement> mappedStatements; // key = "com.example.mapper.UserMapper.selectById" // value = MappedStatement(包含 SQL、参数类型、结果映射等)
3.2 阶段二:运行时阶段(执行时)
目标:执行 SQL,返回结果。
| 核心组件 | 职责 |
|---|---|
SqlSession | 对外 API,接收调用 |
Executor | 执行器,调度 SQL 执行 |
StatementHandler | 创建 Statement、执行 SQL |
ParameterHandler | 设置 PreparedStatement 参数 |
ResultSetHandler | 将 ResultSet 映射为对象 |
执行步骤:
text
1. sqlSession.openSession() → 创建 SqlSession 和 Executor 2. sqlSession.getMapper(XxxMapper.class) → 生成 MapperProxy 3. 调用 Mapper 方法 → MapperProxy 拦截 → 根据 "类名+方法名" 从 Configuration 获取 MappedStatement 4. SqlSession 委派给 Executor 5. Executor 执行:缓存判断 → StatementHandler → ParameterHandler → JDBC 执行 → ResultSetHandler 映射 6. 返回结果
3.3 两阶段的关系
text
┌─────────────────────────────────────────────────────────────┐ │ 初始化阶段(启动时) │ │ 配置文件/注解 → 解析 → MappedStatement → 注册到 Configuration │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 运行时阶段(执行时) │ │ Mapper方法调用 → 查找 MappedStatement → Executor 执行 → 返回结果 │ └─────────────────────────────────────────────────────────────┘
🎯 核心观点
MyBatis 的两阶段设计是框架的基石:初始化阶段解析配置、注册 SQL 定义;运行时阶段查找定义、执行 SQL。运行时通过"类名 + 方法名"作为唯一标识查找
MappedStatement,两个阶段彻底解耦。
4. MyBatis 插件机制与分页实现原理
4.1 插件机制概述
MyBatis 允许通过插件(Interceptor)对四大核心组件进行拦截:
| 可拦截组件 | 拦截时机 | 用途示例 |
|---|---|---|
| Executor | SQL 执行前后 | 分页、读写分离、性能监控 |
| StatementHandler | SQL 预处理前后 | SQL 改写、日志记录 |
| ParameterHandler | 参数设置前后 | 参数加密、默认值填充 |
| ResultSetHandler | 结果集处理前后 | 结果脱敏、数据权限 |
插件的本质是 JDK 动态代理,多个插件形成InterceptorChain拦截器链。
4.2 插件执行时机(源码级)
Step 1:创建 Executor 时植入插件
java
// DefaultSqlSessionFactory.openSession() 中 Executor executor = new SimpleExecutor(transaction, autoCommit); // 关键:通过拦截器链生成代理 executor = (Executor) interceptorChain.pluginAll(executor);
pluginAll()遍历所有插件,调用plugin()方法,匹配则返回 JDK 动态代理对象。
Step 2:被代理的方法调用触发intercept()
当调用executor.query()时,实际调用的是代理对象的invoke(),进而触发插件的intercept()。
Step 3:拦截器链形成责任链
text
原始 Executor → 插件1代理 → 插件2代理 → 插件3代理 → ...
4.3 分页插件实现原理
分页插件的核心思路:拦截Executor.query(),在 SQL 执行前动态改写 SQL,添加 LIMIT 语句。
java
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class PagePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取原始 SQL String sql = boundSql.getSql(); // 2. 改写 SQL(添加 LIMIT) String pageSql = sql + " LIMIT " + offset + ", " + limit; // 3. 通过反射替换 BoundSql 中的 SQL,继续执行 return invocation.proceed(); } }4.4 PageHelper 实现思路
拦截
Executor.query(),获取原始 SQL生成 COUNT 查询:
SELECT COUNT(*) FROM (原SQL)执行 COUNT 获取总记录数
改写原 SQL:添加
LIMIT offset, limit(适配数据库方言)执行分页查询,将结果和 total 封装成
Page对象
🎯 核心观点
MyBatis 插件机制基于JDK 动态代理,在
Executor、StatementHandler等核心组件创建时植入代理。分页插件的本质是在运行时动态改写 SQL,通过拦截Executor.query()添加LIMIT语句并生成 COUNT 查询,实现物理分页。
5. MyBatis-Plus 的两阶段增强机制
MyBatis-Plus 遵循"只做增强,不做改变"的理念,在 MyBatis 两阶段架构上"见缝插针"。
5.1 阶段一增强:SQL 注入器(DefaultSqlInjector)
目标:在启动时,为BaseMapper的通用 CRUD 方法自动生成并注入MappedStatement。
| 核心组件 | 职责 |
|---|---|
MybatisMapperRegistry | 继承MapperRegistry,重写addMapper() |
DefaultSqlInjector | 默认 SQL 注入器,包含 BaseMapper 所有方法的 SQL 模板 |
AbstractMethod | 每个 BaseMapper 方法(如selectById)对应的 SQL 生成器 |
TableInfo | 实体类元数据(表名、字段名、主键等) |
执行流程:
text
1. Spring Boot 启动,MybatisPlusAutoConfiguration 生效 2. 扫描 Mapper 接口(如 UserMapper) 3. MybatisMapperRegistry.addMapper(UserMapper.class) → 继承自 MapperRegistry,重写了 addMapper 方法 4. addMapper() 内部调用 DefaultSqlInjector.inspectInject(UserMapper.class) 5. DefaultSqlInjector 获取 BaseMapper 中所有预定义方法列表 6. 遍历每个方法(如 selectById、insert): a. 从 SqlMethod 枚举获取 SQL 模板(如 "SELECT %s FROM %s WHERE %s=#{%s}") b. 通过 TableInfo 获取实体类元数据(表名、字段名等) c. 填充模板,生成完整 SQL d. 创建 MappedStatement,注册到 Configuration关键示例(selectById的 SQL 生成):
java
// SqlMethod 枚举中定义模板 SELECT_BY_ID("selectById", "SELECT %s FROM %s WHERE %s=#{%s}", ...); // 填充后:SELECT id, name, age, email FROM user WHERE id=#{id}注入时机链路:
text
MybatisMapperFactoryBean.checkDaoConfig() → Configuration.addMapper() → MybatisMapperRegistry.addMapper() → DefaultSqlInjector.inspectInject()
5.2 阶段二增强:MybatisPlusInterceptor
目标:在运行时,通过拦截器对 SQL 执行过程进行增强(分页、乐观锁、多租户等)。
| 核心组件 | 职责 |
|---|---|
MybatisPlusInterceptor | MP 核心拦截器,代理 Executor 和 StatementHandler |
InnerInterceptor | 内部拦截器接口,所有具体插件实现此接口 |
PaginationInnerInterceptor | 分页插件 |
TenantLineInnerInterceptor | 多租户插件 |
OptimisticLockerInnerInterceptor | 乐观锁插件 |
执行流程:
text
1. Mapper 方法被调用(如 userMapper.selectPage(page, wrapper)) 2. MapperProxy → SqlSession → Executor 3. Executor 是被 MybatisPlusInterceptor 代理的对象 4. MybatisPlusInterceptor.intercept() 被触发 5. 遍历内部所有 InnerInterceptor: a. PaginationInnerInterceptor:有分页参数则改写 SQL(添加 COUNT 和 LIMIT) b. TenantLineInnerInterceptor:需要多租户则追加 tenant_id 条件 c. OptimisticLockerInnerInterceptor:有 @Version 则修改 WHERE 条件 6. 所有 InnerInterceptor 执行完毕,调用 invocation.proceed() 执行真实 JDBC 7. 返回增强后的结果(如 Page 对象包含 total + records)
5.3 两阶段增强总结
text
┌─────────────────────────────────────────────────────────────────────┐ │ MyBatis-Plus 两阶段增强 │ ├─────────────────────────────────────────────────────────────────────┤ │ 阶段一(启动时):SQL 注入器(DefaultSqlInjector) │ │ ├── MybatisMapperRegistry.addMapper() 重写 │ │ ├── DefaultSqlInjector.inspectInject() 遍历 BaseMapper 方法 │ │ ├── 从 SqlMethod 获取 SQL 模板,用 TableInfo 填充 │ │ └── 注册 MappedStatement 到 Configuration │ │ 解决的问题:BaseMapper 的 SQL 从哪来? │ ├─────────────────────────────────────────────────────────────────────┤ │ 阶段二(运行时):拦截器(MybatisPlusInterceptor) │ │ ├── 代理 Executor.query() / update() 方法 │ │ ├── 遍历 InnerInterceptor 列表(分页/多租户/乐观锁) │ │ ├── 按需改写 SQL(添加 LIMIT、tenant_id、版本号条件) │ │ └── 执行真实 JDBC 操作 │ │ 解决的问题:分页/租户/乐观锁的 SQL 如何动态改写? │ └─────────────────────────────────────────────────────────────────────┘
🎯 核心观点
MyBatis-Plus 的增强本质上是两阶段各插一脚:
阶段一(启动时):通过
MybatisMapperRegistry重写addMapper(),用DefaultSqlInjector主动注入BaseMapper的 SQL 定义阶段二(运行时):通过
MybatisPlusInterceptor统一代理,将分页、多租户、乐观锁等增强逻辑以InnerInterceptor形式按需织入SQL 执行过程
6. MP 插件配置与 MyBatis 原生配置的区别
6.1 核心区别
| 维度 | MyBatis 原生 | MyBatis-Plus |
|---|---|---|
| 插件接口 | 直接实现Interceptor | 实现InnerInterceptor,由MybatisPlusInterceptor统一代理 |
| 注册方式 | 每个插件单独注册到Configuration | 所有插件添加到MybatisPlusInterceptor的interceptors列表 |
| 配置类 | SqlSessionFactoryBean.setPlugins() | MybatisPlusInterceptor.addInnerInterceptor() |
| 拦截粒度 | 拦截四大组件的方法 | 统一拦截后分发给InnerInterceptor |
6.2 配置代码对比
MyBatis 原生(Java Config):
java
@Configuration public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setPlugins(new Interceptor[]{new PagePlugin()}); return factory.getObject(); } }MyBatis-Plus(Java Config):
java
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }6.3 为什么 MP 要重新设计插件配置?
统一管理:所有增强功能通过一个拦截器管理,避免多个插件互相干扰
简化使用:开发者只需关注
InnerInterceptor的业务逻辑,无需关心 MyBatis 的拦截细节顺序可控:通过
addInnerInterceptor()的顺序控制插件执行顺序更好的扩展性:新增插件只需实现
InnerInterceptor
🎯 核心观点
MyBatis-Plus 的插件配置与 MyBatis 原生配置的本质区别在于:从"多个独立插件"演进为"一个统一拦截器 + 多个内部增强器"。MP 通过
MybatisPlusInterceptor统一代理,降低了多插件之间的耦合,提供了更简洁、顺序可控的扩展方式,同时完全兼容MyBatis 原生配置。
📝 总结
本文从六个维度深入剖析了 MyBatis 和 MyBatis-Plus 的底层原理:
| 维度 | MyBatis | MyBatis-Plus |
|---|---|---|
| JDBC 封装 | SqlSession → Executor → StatementHandler → ResultSetHandler | 完全复用 |
| Spring Boot 配置 | MybatisAutoConfiguration | MybatisPlusAutoConfiguration |
| 两阶段设计 | 启动注册 + 运行执行 | 启动注入 SQL + 运行拦截增强 |
| 插件机制 | 4 个拦截点 + 链式调用 | 统一拦截器 + InnerInterceptor |
| SQL 来源 | XML/注解静态定义 | BaseMapper 动态注入 |
核心思想:
MyBatis 的底层本质是"两阶段设计 + 插件机制":
两阶段:启动时注册 SQL 定义,运行时查找并执行
插件:在 Executor 等核心组件创建时植入动态代理
MyBatis-Plus 没有重写 MyBatis,而是在其两阶段架构上"见缝插针":
启动时注入 SQL(
DefaultSqlInjector)运行时拦截增强(
MybatisPlusInterceptor)
理解了这个本质,你就能更好地驾驭这两个框架,遇到问题时也能快速定位是启动阶段还是运行阶段的问题。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注!
你的支持是我持续输出优质技术文章的最大动力