01o00o10

MyBatis-Plus 源码阅读(四):BaseMapper 的 SQL 是什么时候塞进 MyBatis 的

MyBatis-Plus 源码阅读(四):BaseMapper 的 SQL 是什么时候塞进 MyBatis 的

BaseMapper 只有方法声明,却能执行 SQL。真正把两者接起来的,是 SQL 注入器。

先再强调一次:这里的“注入”是把 SQL 定义注册到 MyBatis,不是安全漏洞里的 SQL 注入。

从 Mapper 注册开始

当扫描器发现 UserMapper 后,最终会调用:

MybatisConfiguration.addMapper(UserMapper.class);

MybatisConfiguration 把工作交给自己的 MybatisMapperRegistry

MybatisMapperRegistry.addMapper()
    -> 先放入 MybatisMapperProxyFactory
    -> 创建 MybatisMapperAnnotationBuilder
    -> parser.parse()

源码会先把 ProxyFactory 放进 knownMappers,再解析 Mapper。这样解析 XML namespace 时,不会反过来重复绑定同一个 Mapper。

如果解析失败,finally 中会把刚放进去的记录移除,避免留下半成品。

Mapper 解析不只是看注解

MybatisMapperAnnotationBuilder.parse() 做了三类事情:

  1. 尝试加载同路径的 Mapper XML;
  2. 解析 Mapper 方法上的 MyBatis 注解;
  3. 如果它是通用 Mapper 的子类,执行 MyBatis-Plus SQL 注入。

第三步的入口是:

parserInjector();

里面的核心代码是:

GlobalConfigUtils
    .getSqlInjector(configuration)
    .inspectInject(assistant, type);

默认拿到的是 DefaultSqlInjector

这里补充一个关键点:getSqlInjector() 返回的是 ISqlInjector 接口的实现。ISqlInjector 定义了 SQL 注入的契约,只有一个核心方法 inspectInject

public interface ISqlInjector {
    void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}

AbstractSqlInjector 实现了这个接口,提供了注入流程的骨架。如果你想自定义通用方法,可以实现 ISqlInjector 接口,或者继承 AbstractSqlInjector 抽象类。

AbstractSqlInjector 做了什么

inspectInject() 先从 Mapper 泛型中找到实体类,再初始化 TableInfo。但在这之前,有一个容易被忽略的步骤——检查缓存:

Set<String> mapperRegistryCache =
    GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());

if (!mapperRegistryCache.contains(className)) {
    // 初始化 TableInfo,获取方法列表,逐个注入
    // ...
    mapperRegistryCache.add(className);
}

mapperRegistryCache 缓存了已经完成 CRUD 注入的 Mapper 信息。如果同一个 Mapper 被重复解析(例如被多个扫描器扫到),缓存会阻止重复注入。

通过缓存检查后,流程如下:

UserMapper
    -> BaseMapper<User>
    -> User.class
    -> TableInfo

然后获取本次要注入的方法列表,最后逐个调用:

method.inject(
    builderAssistant,
    mapperClass,
    modelClass,
    tableInfo
);

每个 AbstractMethod 子类负责一条通用方法,例如 InsertSelectListUpdateById

默认到底注入哪些方法

DefaultSqlInjectorgetMethodList() 会先加入以下方法:

insert
delete
update
selectCount
selectMaps
selectObjs
selectList

如果实体有主键,再加入:

deleteById
deleteByIds
updateById
selectById
selectByIds

这也解释了为什么没有主键的实体还能 selectList,却不能正常使用 selectById

BaseMapper 里还有一些 default 方法。它们可能复用上面的基础 MappedStatement,并不一定每个 Java 方法都对应一个独立的注入类。

inject 最后注册了什么

AbstractMethod.inject() 会准备当前 Configuration、LanguageDriver、Mapper 类、实体类和 TableInfo,随后调用子类的 injectMappedStatement()

inject() 内部还有一个容易被忽略的判断——如果该 ID 对应的 MappedStatement 已经存在于 Configuration 中,则直接跳过。这是 XML 覆盖通用方法的根本原因:XML 先解析并注册了同名的 MappedStatement,后续注入时检测到已存在便不再覆盖。

SelectList 为例,它会生成 SQL 脚本,交给 LanguageDriver 转成 SqlSource,再通过 BuilderAssistant 注册:

ID: com.example.mapper.UserMapper.selectList
SqlCommandType: SELECT
SqlSource: DynamicSqlSource 或 RawSqlSource
ResultMap: User 对应的结果映射

这里需要修正一个常见误解:SelectList 生成的 SqlSource 不一定是 DynamicSqlSource。具体类型取决于 SQL 脚本中是否包含动态标签(如 <if>):包含动态标签时生成 DynamicSqlSource,纯静态 SQL 则生成 RawSqlSource。对于 SelectList 这类需要处理条件构造器的方法,通常生成的是 DynamicSqlSource,但如果条件构造器逻辑被优化为静态拼接,则可能是 RawSqlSource

到这里,MyBatis 已经拥有执行 selectList 所需的定义。

为什么 XML 可以覆盖通用方法

MybatisConfiguration 对同名 MappedStatement 做了检查。

Mapper 解析时会先加载 XML,再解析注解,最后注入通用 CRUD。因此同一个 ID 已经由 XML 提供时,后面的通用语句不会再覆盖它。

源码里给出的优先级是:

XML SQL > SqlProvider SQL > 通用 CRUD SQL

你可以在 Mapper XML 中重新定义 selectList,但这么做之前要想清楚:调用方通常以为它还是通用查询。

怎么验证

项目启动后取出 Configuration:

Configuration configuration =
    sqlSessionFactory.getConfiguration();

String id =
    UserMapper.class.getName() + ".selectList";

assertTrue(configuration.hasStatement(id));

MappedStatement ms =
    configuration.getMappedStatement(id);

assertEquals(
    SqlCommandType.SELECT,
    ms.getSqlCommandType()
);

推荐断点:

  1. MybatisMapperRegistry.addMapper()
  2. MybatisMapperAnnotationBuilder.parse()
  3. MybatisMapperAnnotationBuilder.parserInjector()
  4. AbstractSqlInjector.inspectInject()
  5. AbstractMethod.inject()

在第五个断点记录当前 methodName,会看到一个 Mapper 启动时连续注册多条语句。

posted on 2026-09-21 10:01  01o00o10  阅读(4)  评论(0)    收藏  举报

导航