【MyBatis-Plus】源码级吃透 MyBatis 与 MyBatis-Plus:两阶段架构、插件机制与底层增强全解析

作者:大家好,我是 CodeStats。一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。

我的技术信条:所有高深的技术,最后都能用大白话讲清楚。如果讲不清楚,说明还没真正理解。


📖 本文收获

  • ✅ 理解 MyBatis 如何封装 JDBC,通过SqlSession完成数据库操作

  • ✅ 掌握 Spring Boot 自动配置 MyBatis 的底层原理

  • ✅ 深入 MyBatis两阶段设计:初始化阶段 + 运行时阶段

  • ✅ 系统学习 MyBatis插件机制,理解分页实现原理

  • ✅ 掌握 MyBatis-Plus 的两阶段增强:MapperRegistryDefaultSqlInjectorMybatisPlusInterceptor

  • ✅ 理清 MP 插件配置与 MyBatis 原生配置的本质区别


📑 目录

  1. MyBatis 如何封装 JDBC 通过 SqlSession 执行?

  2. Spring Boot 如何自动配置 SqlSessionFactory?

  3. MyBatis 两阶段设计的核心组件与执行步骤

  4. MyBatis 插件机制与分页实现原理

  5. MyBatis-Plus 的两阶段增强机制

  6. 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 的封装,核心解决四个问题:

  1. 安全执行 SQL,抽象掉 JDBC 的复杂性

  2. 将 Java 参数对象映射到 JDBC PreparedStatement

  3. 将 JDBC ResultSet 的行映射为 Java 对象

  4. 动态生成 SQL(XML 标签或注解)

核心组件体系

组件职责
SqlSessionFactory创建 SqlSession(工厂模式)
SqlSession对外 API,定义 CRUD 方法(门面模式)
ExecutorSQL 执行器,封装 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);

通过MapperRegistryJDK 动态代理生成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-locationsconfig-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 3buildSqlSessionFactory()内部创建Configuration,解析 XML、注册类型处理器、插件等

SqlSessionTemplate 与 Mapper 扫描

  • SqlSessionTemplate:线程安全,与 Spring 事务管理无缝集成

  • @MapperScan:通过MapperScannerConfigurer将 Mapper 接口注册为MapperFactoryBean

🎯 核心观点

Spring Boot 自动配置 MyBatis 的核心流程:spring.factoriesMybatisAutoConfigurationSqlSessionFactoryBean构建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)对四大核心组件进行拦截:

可拦截组件拦截时机用途示例
ExecutorSQL 执行前后分页、读写分离、性能监控
StatementHandlerSQL 预处理前后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 实现思路

  1. 拦截Executor.query(),获取原始 SQL

  2. 生成 COUNT 查询:SELECT COUNT(*) FROM (原SQL)

  3. 执行 COUNT 获取总记录数

  4. 改写原 SQL:添加LIMIT offset, limit(适配数据库方言)

  5. 执行分页查询,将结果和 total 封装成Page对象

🎯 核心观点

MyBatis 插件机制基于JDK 动态代理,在ExecutorStatementHandler等核心组件创建时植入代理。分页插件的本质是在运行时动态改写 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 执行过程进行增强(分页、乐观锁、多租户等)。

核心组件职责
MybatisPlusInterceptorMP 核心拦截器,代理 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所有插件添加到MybatisPlusInterceptorinterceptors列表
配置类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 要重新设计插件配置?

  1. 统一管理:所有增强功能通过一个拦截器管理,避免多个插件互相干扰

  2. 简化使用:开发者只需关注InnerInterceptor的业务逻辑,无需关心 MyBatis 的拦截细节

  3. 顺序可控:通过addInnerInterceptor()的顺序控制插件执行顺序

  4. 更好的扩展性:新增插件只需实现InnerInterceptor

🎯 核心观点

MyBatis-Plus 的插件配置与 MyBatis 原生配置的本质区别在于:从"多个独立插件"演进为"一个统一拦截器 + 多个内部增强器"。MP 通过MybatisPlusInterceptor统一代理,降低了多插件之间的耦合,提供了更简洁、顺序可控的扩展方式,同时完全兼容MyBatis 原生配置。


📝 总结

本文从六个维度深入剖析了 MyBatis 和 MyBatis-Plus 的底层原理:

维度MyBatisMyBatis-Plus
JDBC 封装SqlSession → Executor → StatementHandler → ResultSetHandler完全复用
Spring Boot 配置MybatisAutoConfigurationMybatisPlusAutoConfiguration
两阶段设计启动注册 + 运行执行启动注入 SQL + 运行拦截增强
插件机制4 个拦截点 + 链式调用统一拦截器 + InnerInterceptor
SQL 来源XML/注解静态定义BaseMapper 动态注入

核心思想

MyBatis 的底层本质是"两阶段设计 + 插件机制"

  • 两阶段:启动时注册 SQL 定义,运行时查找并执行

  • 插件:在 Executor 等核心组件创建时植入动态代理

MyBatis-Plus 没有重写 MyBatis,而是在其两阶段架构上"见缝插针"

  • 启动时注入 SQL(DefaultSqlInjector

  • 运行时拦截增强(MybatisPlusInterceptor

理解了这个本质,你就能更好地驾驭这两个框架,遇到问题时也能快速定位是启动阶段还是运行阶段的问题。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注!

你的支持是我持续输出优质技术文章的最大动力