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() 做了三类事情:
- 尝试加载同路径的 Mapper XML;
- 解析 Mapper 方法上的 MyBatis 注解;
- 如果它是通用 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 子类负责一条通用方法,例如 Insert、SelectList、UpdateById。
默认到底注入哪些方法
DefaultSqlInjector 的 getMethodList() 会先加入以下方法:
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()
);
推荐断点:
MybatisMapperRegistry.addMapper()MybatisMapperAnnotationBuilder.parse()MybatisMapperAnnotationBuilder.parserInjector()AbstractSqlInjector.inspectInject()AbstractMethod.inject()
在第五个断点记录当前 methodName,会看到一个 Mapper 启动时连续注册多条语句。
浙公网安备 33010602011771号