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,观察租户插件。

进行一步一步的深入解析

第一次调试,断点打在以下四个断点:

  1. MybatisPlusAutoConfiguration.sqlSessionFactory()
  2. MybatisMapperAnnotationBuilder.parse()
  3. AbstractSqlInjector.inspectInject()
  4. MybatisPlusInterceptor.intercept()

启动项目后,在第三个断点观察 MyBatis 的 Configuration。

等 UserMapper 解析完成,里面应该已经存在:

com.example.mapper.UserMapper.selectList

这时业务代码还没有调用 selectList,但它对应的 MappedStatement 已经准备好了。

然后执行测试,请求会进入第四个断点。这里看到的就是运行期插件链。

把这两个时刻对上,MyBatis-Plus 最重要的“两阶段模型”就建立起来了:

启动时注册 SQL
运行时交给 MyBatis 执行
posted @ 2026-09-20 16:23  01o00o10  阅读(27)  评论(0)    收藏  举报