MyBatis-Plus 源码阅读(一):先别急着点进 BaseMapper
MyBatis-Plus 源码阅读(一):先别急着点进 BaseMapper
很多人第一次读 MyBatis-Plus 源码,都会从 BaseMapper 开始。
毕竟平时写得最多的就是它,如:
public interface UserMapper extends BaseMapper<User> {
}
点进去一看,BaseMapper 还是个接口。再点 selectList,只有一个方法声明,没有实现。
奇怪的地方来了:一个没有实现类的方法,为什么真的能查数据库?
如果顺着 IDE 一路跳,很快就会碰到 MappedStatement、SqlSource、LanguageDriver、MapperProxy。每个类好像都认识,连起来又不知道在干什么。
所以第一篇先不钻某个方法。先把整体流程看明白,后面再遇到这些类,才知道它为什么会出现在这里。
从一段最普通的查询说起
先看一段熟悉的代码:
@Data
@TableName("mp_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
}
public interface UserMapper extends BaseMapper<User> {
}
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.ge(User::getAge, 18)
.orderByDesc(User::getId)
);
最后执行的 SQL 大概是:
SELECT id, name, age
FROM mp_user
WHERE age >= ?
ORDER BY id DESC
代码不长,背后要做的事情却不少:
- 得知道
User对应哪张表; - 得知道
User::getAge对应age字段; - 得为
selectList准备 SQL; - 得把参数安全地放进占位符;
- 最后还得把查询结果装回
User。
这些事情并不全是 MyBatis-Plus 做的。
简单说,MyBatis-Plus 负责把常用的材料提前准备好,真正执行 SQL 的还是 MyBatis。
这就是我们读源码时最先要分清的边界。
MyBatis-Plus 到底帮我们做了什么
先不背类名,按平时写代码的场景来看。
第一件事:少写重复的 CRUD
没有 MyBatis-Plus 时,一个普通的单表 Mapper 往往要写不少重复 SQL。用了 BaseMapper 以后,常见方法已经准备好了:
userMapper.insert(user);
userMapper.deleteById(id);
userMapper.updateById(user);
userMapper.selectById(id);
userMapper.selectList(wrapper);
这些方法不是某个通用实现类直接完成的。
MyBatis-Plus 会在项目启动时,根据实体信息生成 SQL,并把它们注册到 MyBatis。等业务代码真正调用 Mapper 时,MyBatis 会像执行 XML 中的 SQL 一样执行它们。
这一点很重要,后面读 SQL 注入器时还会回来讲。
第二件事:帮我们描述查询条件
QueryWrapper 和 LambdaQueryWrapper 做的事情,可以先理解成“组装 SQL 条件和参数”。
例如:
Wrappers.<User>lambdaQuery()
.eq(User::getName, "Alice")
.ge(User::getAge, 18);
它会准备类似下面的条件:
name = ? AND age >= ?
同时把 Alice 和 18 放进自己的参数集合里。
Wrapper 不负责连接数据库,也不负责执行 SQL。它只是把“查什么”描述清楚,然后交给 MyBatis 的动态 SQL 系统。
第三件事:管理实体和字段
MyBatis-Plus 需要知道:
- 实体对应哪张表;
- 哪个属性是主键;
- 哪些字段参与插入和更新;
- 哪个字段是逻辑删除标记;
- 哪个字段是乐观锁版本号;
- 属性名和列名如何转换。
这些信息最后会集中到 TableInfo 和 TableFieldInfo 中。
所以后面你会发现,主键生成、自动填充、逻辑删除和通用 SQL 注入,看上去是不同功能,底层却都会来找 TableInfo。
它相当于 MyBatis-Plus 认识一个实体之后,整理出来的那份档案。
第四件事:在 SQL 执行前动点手脚
分页、多租户、数据权限、乐观锁这类功能,不太适合在每条业务 SQL 里手写。
MyBatis-Plus 把它们放进插件体系:
MybatisPlusInterceptor
├── PaginationInnerInterceptor
├── OptimisticLockerInnerInterceptor
├── TenantLineInnerInterceptor
├── DataPermissionInterceptor
└── BlockAttackInnerInterceptor
MybatisPlusInterceptor 是一个标准 MyBatis 插件,里面再挂多个 InnerInterceptor。
查询或更新经过 MyBatis 的 Executor、StatementHandler 时,这些内部拦截器就有机会检查 SQL、修改 SQL,或者直接阻止它继续执行。
第五件事:提供一些更顺手的上层 API
除了核心的 Mapper 和 Wrapper,这系列的源码里还有:
IService、ServiceImpl和 Repository;- 批量插入、批量更新;
- ActiveRecord;
- 链式查询;
Db和SimpleQuery;- 枚举、JSON 类型处理;
- 代码生成器;
- Spring Boot 自动配置。
这些功能很实用,但读源码时不能一上来全铺开。
一个很关键的区别:启动时做,还是查询时做
MyBatis-Plus 的代码看起来很多,但按执行时间分一下,会清楚不少。
项目启动时
项目启动时,MyBatis-Plus 主要在准备东西:
Spring Boot 自动配置
-> 创建 SqlSessionFactory
-> 创建 MybatisConfiguration
-> 扫描 Mapper
-> 解析实体信息
-> 生成通用 CRUD SQL
-> 注册 MappedStatement
其中一条重要调用链是:
MybatisPlusAutoConfiguration
-> MybatisSqlSessionFactoryBean
-> MybatisConfiguration
-> MybatisMapperAnnotationBuilder
-> AbstractSqlInjector
-> TableInfoHelper
-> DefaultSqlInjector
-> AbstractMethod
-> MappedStatement
这一阶段只要明白一件事:BaseMapper 中那些没有实现的方法,会在这里拿到对应的 SQL 定义。
业务查询时
等到下面这行代码真正执行:
userMapper.selectList(wrapper);
调用就回到了我们熟悉的 MyBatis 主流程:
Mapper 接口代理
-> MapperMethod
-> SqlSession
-> Executor
-> StatementHandler
-> JDBC
-> ResultSetHandler
MyBatis-Plus 会在几个位置加入自己的处理:
- Wrapper 提供查询条件和参数;
MybatisParameterHandler处理主键生成和字段填充;MybatisPlusInterceptor执行分页、乐观锁等插件;- TypeHandler 处理枚举、JSON 等特殊类型。
但执行主干还是 MyBatis。
所以,“MyBatis-Plus 是不是重新实现了一个 ORM?”这个问题可以直接回答:没有。
它是在 MyBatis 已有的入口上,把常用能力补齐了。
BaseMapper 没有实现类,方法到底怎么跑
现在回到开头的问题。
MyBatis 本来就不要求 Mapper 接口有普通 Java 实现类。它会为 Mapper 创建代理对象,然后根据“接口名 + 方法名”查找对应的 SQL 定义。
例如:
com.example.mapper.UserMapper.selectList
这个字符串就是一条 MappedStatement 的 ID。
原生 MyBatis 一般从 Mapper XML 或 @Select 之类的注解中注册 MappedStatement。MyBatis-Plus 又加了一种来源:SQL 注入器。
BaseMapper 负责声明方法
DefaultSqlInjector 负责挑选要注入的方法
SelectList 等 AbstractMethod 子类负责生成 SQL
MyBatisConfiguration 保存最终的 MappedStatement
到了运行时,Mapper 代理只认这个 ID,不关心 SQL 最初来自 XML、注解,还是 MyBatis-Plus。
这就是 BaseMapper 不需要实现类的原因。
顺便说一个容易误会的地方:这里的“SQL 注入”不是安全漏洞里的 SQL Injection。
MyBatis-Plus 说的 SQL Injector,是把一条 SQL 定义注册进 MyBatis Configuration。
两个词一样,完全不是一回事。
逻辑删除为什么不在插件里
很多人第一次找逻辑删除源码,会先去 plugins 包里搜。
结果找不到一个叫 LogicDeleteInterceptor 的东西。
原因是逻辑删除主要发生在生成通用 SQL 的阶段。
假设实体中有:
@TableLogic
private Integer deleted;
DeleteById 在生成 SQL 时会检查 TableInfo。发现有逻辑删除字段后,它注册的不再是普通的:
DELETE FROM mp_user WHERE id = ?
而会变成类似:
UPDATE mp_user
SET deleted = 1
WHERE id = ? AND deleted = 0
查询方法生成 SQL 时,也会把 deleted = 0 放进条件。
因此可以先记住:
- 逻辑删除主要是 SQL 生成期能力;
- 主键生成和自动填充主要是参数处理期能力;
- 分页、租户、乐观锁主要是执行拦截期能力。
以后找源码,先判断功能发生在哪个阶段,能少走很多弯路。
源码模块怎么分
当前仓库不是一个单模块项目,主要依赖方向是:
mybatis-plus-annotation
↓
mybatis-plus-core
↓
mybatis-plus-extension
↓
mybatis-plus-spring
↓
mybatis-plus
↓
Spring Boot Starter
每个模块大概负责什么,可以这样看:
| 模块 | 放的是什么 |
|---|---|
mybatis-plus-annotation |
@TableName、@TableId、@TableLogic、@Version 等注解 |
mybatis-plus-core |
BaseMapper、Wrapper、TableInfo、SQL 注入器和 MyBatis 核心扩展 |
mybatis-plus-extension |
插件框架、分页模型、Repository、链式 API 和各种 TypeHandler |
mybatis-plus-spring |
Spring 下的 Service、Repository、事务批处理和 SessionFactoryBean |
mybatis-plus-jsqlparser-* |
分页、租户、数据权限和 SQL 安全等 SQL 解析能力 |
mybatis-plus-generator |
数据库元数据读取和代码生成 |
spring-boot-starter |
Spring Boot 2、3、4 自动配置 |
mybatis-plus |
把常用模块聚合起来 |
读源码时,推荐顺着依赖方向往下读:
annotation -> core -> extension -> spring -> starter
JSqlParser 插件和代码生成器可以放到后面。它们很有用,但不是理解通用 CRUD 的前提。
MyBatis 做什么,MyBatis-Plus 又做什么
最后把两者分工摆在一起:
| 这件事 | 主要由谁完成 |
|---|---|
| 创建 Mapper 代理 | MyBatis |
| 执行 SQL、管理缓存、映射结果 | MyBatis |
提供 SqlSession、Executor、StatementHandler |
MyBatis |
| 从 XML 和注解解析 SQL | MyBatis |
| 根据实体生成通用 CRUD | MyBatis-Plus |
解析 @TableName、@TableId 等注解 |
MyBatis-Plus |
| 组织 Wrapper 条件 | MyBatis-Plus |
| 自动填充和分配 ID | MyBatis-Plus |
| 分页、乐观锁、租户等 SQL 增强 | MyBatis-Plus 借助 MyBatis 插件机制 |
可以把它们的关系理解成:
MyBatis 把路修好了,MyBatis-Plus 在这条路上增加了常用路线和交通规则。
它没有绕开 MyBatis 另起一套执行流程。
动手验证一下
整个系列可以共用一个 Spring Boot 3 + H2 小项目。
依赖如下:
dependencies {
implementation 'com.baomidou:mybatis-plus-spring-boot3-starter:3.5.17'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.yml:
spring:
datasource:
url: jdbc:h2:mem:mp_reading;MODE=MySQL;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
sql:
init:
mode: always
schema.sql:
CREATE TABLE mp_user (
id BIGINT PRIMARY KEY,
name VARCHAR(64),
age INT
);
INSERT INTO mp_user(id, name, age) VALUES
(1, 'Alice', 20),
(2, 'Bob', 16);
启动类扫描 Mapper:
@SpringBootApplication
@MapperScan("com.example.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
写一个测试:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void shouldQueryAdultUsers() {
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.ge(User::getAge, 18)
);
assertEquals(1, users.size());
assertEquals("Alice", users.get(0).getName());
}
}
后面的文章就在这张表上继续加字段:
- 加
create_time,观察自动填充; - 加
deleted,观察逻辑删除; - 加
version,观察乐观锁; - 加
tenant_id,观察租户插件。
进行一步一步的深入解析
第一次调试,断点打在以下四个断点:
MybatisPlusAutoConfiguration.sqlSessionFactory()MybatisMapperAnnotationBuilder.parse()AbstractSqlInjector.inspectInject()MybatisPlusInterceptor.intercept()
启动项目后,在第三个断点观察 MyBatis 的 Configuration。
等 UserMapper 解析完成,里面应该已经存在:
com.example.mapper.UserMapper.selectList
这时业务代码还没有调用 selectList,但它对应的 MappedStatement 已经准备好了。
然后执行测试,请求会进入第四个断点。这里看到的就是运行期插件链。
把这两个时刻对上,MyBatis-Plus 最重要的“两阶段模型”就建立起来了:
启动时注册 SQL
运行时交给 MyBatis 执行
本文来自博客园,作者:01o00o10,转载请注明原文链接:https://www.cnblogs.com/01o00o10/articles/23050340

浙公网安备 33010602011771号