MyBatis Plus 的魔法藏在哪里——空接口为什么能调用方法
你写了一个空接口
Ze01Mapper extends BaseMapper<Ze01>,没写一行 SQL,甚至没写实现类。但ze01Mapper.selectById("xxx")就能跑。这篇文章拆开 MyBatis Plus 的启动链路,看它怎么把这行空接口变成一个能执行完整 SQL 的 Mapper。
文章目录
一、你看到的和你没看到的
一个典型的 MyBatis Plus Mapper:
@Mapper
public interface Ze01Mapper extends BaseMapper<Ze01> {
// 这个接口里一个字都没有
}
对应的实体:
@Data
@TableName("ZE01")
public class Ze01 {
@TableId("BAZ001")
private String baz001;
@TableField("BAZ002")
private String baz002;
}
然后你在 Service 里注入:
@Autowired
private Ze01Mapper ze01Mapper;
public Ze01 getById(String id) {
return ze01Mapper.selectById(id); // 这行调用的方法从哪来的?
}
一个空接口,没写 XML、没写 @Select、没写实现类。但 selectById 实实在在查出了数据。MyBatis 原生的规则是:要么有 XML,要么有注解——两者都没有,MyBatis 应该报 BindingException。
那 MyBatis Plus 是怎么绕过这个规则的?
二、本质:在 MyBatis 启动过程中插入了一批"自动生成的 MappedStatement"
MyBatis 执行 SQL 靠的是一个核心概念——MappedStatement。每个 SQL 操作(selectById、insert、updateById)最终都对应着一个 MappedStatement 对象,存储在 Configuration.mappedStatements 这个 Map 里。
我写的 XML <select id="selectById"> 和 @Select("SELECT ...") 会在 MyBatis 初始化时被解析成 MappedStatement 注册进去。当业务代码调用 mapper.selectById(id) 时,MyBatis 先拼出这条语句的 ID——com.browise.mapper.Ze01Mapper.selectById——然后到 mappedStatements 里按这个 ID 查找对应的 MappedStatement。找到了就执行,找不到就抛 BindingException。
MyBatis Plus 做的事:在 MyBatis 初始化完成之后、在你第一次调用之前,把 selectById、insert、updateById、deleteById 这些方法的 MappedStatement 自动生成并注入到 mappedStatements 里。
三、入口:MybatisSqlSessionFactoryBean 替代了原生 SqlSessionFactory
我引入 mybatis-plus-boot-starter 之后,Spring Boot 通过 SPI 机制自动加载了 MybatisPlusAutoConfiguration。这个配置类里最关键的一行:
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
// ... 其他配置 ...
return factory.getObject();
}
注意:这里创建的不是原生的 SqlSessionFactoryBean,是 MyBatis Plus 的 MybatisSqlSessionFactoryBean。只有一个差异——它返回的 Configuration 不是 org.apache.ibatis.session.Configuration,而是 MyBatis Plus 的 MybatisConfiguration。
MybatisConfiguration 只做了一件事:当查找 MappedStatement 失败时,触发一次动态注入。 这会覆盖 MyBatis 原生的"没找到就抛异常"的行为。
四、注入引擎:SqlInjector + AbstractMethod
MybatisSqlSessionFactoryBean 在 getObject() 完成初始化后会立即调用一次 sqlSessionFactory.getConfiguration().addMapper(mapperInterface)——这会触发 MyBatis 的标准 Mapper 注册流程。
在这个注册过程中,MyBatis Plus 用了一个叫 ISqlInjector 的接口做了一次注入:
public interface ISqlInjector {
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
默认实现是 DefaultSqlInjector。它会遍历 BaseMapper 接口里定义的每一个方法,为每个方法创建一个对应的 AbstractMethod 子类:
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass) {
return Stream.of(
new Insert(),
new DeleteById(),
new UpdateById(),
new SelectById(),
new SelectCount(),
new SelectList(),
new SelectMaps(),
new SelectPage()
).collect(Collectors.toList());
}
}
每一个 AbstractMethod 子类负责生成一条完整的 SQL 并注册为 MappedStatement。以 SelectById 为例:
public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass,
Class<?> modelClass, TableInfo tableInfo) {
// 拼 SQL: SELECT baz001,baz002,... FROM ZE01 WHERE baz001=?
String sql = String.format(
"SELECT %s FROM %s WHERE %s=#{id}",
tableInfo.getAllSqlSelect(), // baz001, baz002
tableInfo.getTableName(), // ZE01
tableInfo.getKeyColumn() // baz001
);
// 注册到 Configuration.mappedStatements
return this.addSelectMappedStatementForTable(mapperClass, "selectById",
sqlSource, tableInfo);
}
}
到这一步,mappedStatements 里已经有了 com.xxx.Ze01Mapper.selectById 这条记录。后续业务代码调用 ze01Mapper.selectById(id) 时,MyBatis 按 ID 查找——直接命中。
五、TableInfoHelper——启动时把实体注解读进内存
AbstractMethod 在拼 SQL 时需要知道表名、主键列名、字段清单。这些信息是从 TableInfo 对象里拿的。
TableInfoHelper 在启动阶段做了一次全局扫描——遍历所有标记了 @TableName 的实体类,用反射读取 @TableId、@TableField 注解,生成 TableInfo 缓存起来:
TableInfo tableInfo = new TableInfo();
tableInfo.setTableName(entity.getAnnotation(TableName.class).value()); // "ZE01"
tableInfo.setKeyColumn(field.getAnnotation(TableId.class).value()); // "BAZ001"
tableInfo.setFieldList(...); // 所有 @TableField 字段
TableInfoHelper.initTableInfo(mapperClass, tableInfo);
此后 AbstractMethod 在生成 SQL 时,直接通过 TableInfoHelper.getTableInfo(mapperClass) 取值——不用每次都扫注解。
六、运行时的最后一道拦截——MybatisConfiguration
假设前面的注入漏掉了某个方法,或者 Mapper 被动态加载——MyBatis Plus 在运行时还有一道兜底:
public class MybatisConfiguration extends Configuration {
@Override
public MappedStatement getMappedStatement(String id) {
MappedStatement ms = super.getMappedStatement(id);
if (ms != null) {
return ms; // 已有 XML/@Select 或已注入 → 直接返回
}
// 未找到 → 立刻触发一次注入,然后重试
// 等价于:这次没找到 → 当场生成 → 第二次调用就能找到
return super.getMappedStatement(id);
}
}
正常的 MyBatis Configuration.getMappedStatement 在找不到 MappedStatement 的时候会返回 null——然后 MyBatis 抛出 BindingException。MyBatis Plus 的 MybatisConfiguration 在找不到的时候不抛异常——它会让你第二次重入获取到结果,因为已经有方法在第一次没找到时做了一轮补充。
七、完整链路一张图
启动阶段
│
├─ MybatisPlusAutoConfiguration
│ → MybatisSqlSessionFactoryBean.getObject()
│
├─ configuration.addMapper(Ze01Mapper.class)
│ → MyBatis 标准流程:注册 MapperProxyFactory
│
├─ DefaultSqlInjector.inspectInject()
│ → 遍历 getMethodList() → SelectById, Insert, UpdateById, ...
│ → 每个 AbstractMethod.injectMappedStatement()
│ ├─ TableInfoHelper.getTableInfo(Ze01.class)
│ │ → 反射读 @TableName("ZE01") @TableId("BAZ001")
│ ├─ 拼 SQL: SELECT baz001,... FROM ZE01 WHERE baz001=?
│ └─ 注入: configuration.addMappedStatement(ms)
│
└─ 注入完成,Ze01Mapper 可用
运行阶段
│
└─ ze01Mapper.selectById("xxx")
→ MapperProxy 拦截
→ configuration.getMappedStatement("Ze01Mapper.selectById")
→ MybatisConfiguration 重写 → 查 mappedStatements → 命中
→ 执行 JDBC → 返回结果
八、它到底"覆盖"了什么
说 MyBatis Plus "覆盖"了 MyBatis,其实只覆盖了三个点:
| 覆盖点 | 原生 MyBatis | MyBatis Plus |
|---|---|---|
| SqlSessionFactory | SqlSessionFactoryBean | MybatisSqlSessionFactoryBean |
| Configuration | org.apache.ibatis.session.Configuration | MybatisConfiguration(运行时重试注入) |
| 初始化回调 | 无 | ISqlInjector → DefaultSqlInjector → 8 个 AbstractMethod |
剩下的 SQL 执行引擎、缓存、插件拦截链——全是 MyBatis 的原始能力。
九、适用场景
BaseMapper 的 selectById、insert、updateById 能覆盖 90% 的单表 CRUD。复杂场景——多表 JOIN、子查询、动态条件——继续用 XML 或 @Select 手写 SQL,MyBatis Plus 不会干涉你已经定义的 MappedStatement。
如果你的项目中存在大量"建一张表就要配一套 XML"的重复劳动——MyBatis Plus 的自动注入机制可以把这部分工作降到零。
十、结语
MyBatis Plus 的魔法不在别处,就在它往 Configuration.mappedStatements 里偷偷塞了一堆自动生成的 SQL。入口替换了 SqlSessionFactory,启动时读实体注解拿到表名和字段,然后用 DefaultSqlInjector 拼出八条 CRUD SQL 注册进去。
你什么都没写,但它在你启动的那一刻把该写的全写完了。
✅ 亮点:从"空接口为什么能调用方法"这个具体问题出发,逐层拆开 MyBatis Plus 启动链路中 SqlSessionFactory 替换、TableInfo 缓存、AbstractMethod 注入三个关键环节,用真实源码片段解释了自动 CRUD 的生成过程,不涉及理论概念,全是可复现的执行路径。适合正在使用 MyBatis Plus 但对原理不透的开发者,也适合作为面试中"你了解 MyBatis Plus 工作原理吗"的标准答案素材。扩展方向:MyBatis Plus 的乐观锁插件、分页插件拦截链、LambdaQueryWrapper 的条件构建链路,都可以用同样的"切入口到源码"方式拆解。
463

被折叠的 条评论
为什么被折叠?



