Appearance
结论先行
MyBatis-Plus 是 MyBatis 的增强层,不是替代品。不 fork、不修改核心,100% 兼容。实际项目里两者共存,MP 覆盖 80% 单表 CRUD,MyBatis XML 覆盖 20% 复杂场景。
MP 的四层体系
MyBatis 原生链路:SqlSession → Executor → StatementHandler → ParameterHandler/ResultSetHandler。
MP 在它上面加了四层:
Service 层 (IService/ServiceImpl) ← 批量操作、事务
Mapper 层 (BaseMapper<T>) ← 零 XML CRUD
SQL 注入层 (AbstractMethod) ← 自动生成 MappedStatement
元数据层 (TableInfo/TableFieldInfo) ← 实体-表映射缓存每层可以独立替换。比如重写 ServiceImpl.saveBatch(默认是逐条 INSERT 不是真 batch),不影响 Mapper 和注入层。
一个常见的混乱:BaseMapper.selectById 和 IService.getById 功能几乎一样,区别在前者返回 int 后者返回 boolean。团队里得统一规范到底用哪层 API。
SQL 注入:CRUD 自动生成链路
Spring Boot 启动时的注入过程:
MybatisPlusAutoConfiguration
→ MybatisSqlSessionFactoryBean.buildSqlSessionFactory()
→ MybatisMapperRegistry → MapperAnnotationBuilder.parse()
→ MybatisPlusInjector.inspectInject()
→ 遍历所有 AbstractMethod 子类
→ SelectById.injectMappedStatement()每个 CRUD 方法对应一个 AbstractMethod 子类:
java
public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(
Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
SqlSource sqlSource = super.createSqlSource(configuration,
String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(),
tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true)
), Object.class);
return this.addSelectMappedStatementForTable(
mapperClass, methodName, sqlSource, tableInfo);
}
}SQL 模板在 SqlMethod 枚举里定义,占位符由 TableInfo 填充。模板方法模式:子类提供模板和方法名,父类管注册、缓存、ResultMap。
自定义全局方法的扩展方式: 写一个 AbstractMethod 子类 → 注册到注入器 → 所有 Mapper 自动获得该方法。
抽象泄漏: 自定义方法里需要手动调用 tableInfo.getLogicDeleteSql() 这类方法才能支持逻辑删除/多租户——这些本应是"自动"的能力。
TableInfo 缓存
java
public class TableInfo {
private Class<?> entityType;
private String tableName;
private String keyColumn;
private String keyProperty;
private IdType idType;
private List<TableFieldInfo> fieldList;
private boolean withLogicDelete;
}启动时解析,运行时零反射。缓存到 TABLE_INFO_CACHE(Map<Class<?>, TableInfo>)。
初始化:解析 @TableName → 解析 @TableId/@TableField → 构建 ResultMap → 缓存。
运行时改表名/字段映射不会刷新缓存。动态表名插件的替换发生在 SQL 生成的下游,不碰缓存。
伸缩性: 100 个实体类时内存占用几 MB。500+ 时启动注解解析开销累积——遍历字段和方法上的注解是同步完成的。解决方案:按模块拆分 SqlSessionFactory,每个只管理自己模块的 Mapper。
LambdaQueryWrapper 字段名解析
qw.eq(Book::getId, 2) → 提取字段名 id 的链路:
AbstractWrapper.eq → AbstractLambdaWrapper.columnToString → LambdaUtils.extract三种策略降级:
java
public static <T> LambdaMeta extract(SFunction<T, ?> func) {
if (func instanceof Proxy) // 1. IDEA 调试模式
return new IdeaProxyLambdaMeta((Proxy) func);
try {
Method m = func.getClass().getDeclaredMethod("writeReplace");
return new ReflectLambdaMeta(...); // 2. 反射(主路径)
} catch (Throwable e) {
return new ShadowLambdaMeta(...); // 3. 序列化降级
}
}SFunction 继承了 Function + Serializable,编译器为 Lambda 自动生成 writeReplace,返回的 SerializedLambda.implMethodName 即方法名(如 getId)。PropertyNamer.methodToProperty 去掉 get 前缀 + 首字母小写 → id。
Tradeoff 表:
| 决策 | 收益 | 成本 | 风险 |
|---|---|---|---|
| writeReplace 反射解析 | 编译期类型安全,字段重命名自动同步 | 仅支持方法引用,不支持 λ 表达式 | JDK 升级可能改变行为 |
| 反射优先 + 序列化降级 | 主路径纯反射,性能好 | 双路径维护成本 | 极端 JVM 两者都失败 |
| WeakRef 缓存解析结果 | 防类加载器泄漏 | GC 后缓存失效需重解析 | 高 GC 频率下命中率降 |
使用限制:
java
qw.eq(Book::getId, 2); // ✅ 方法引用
qw.eq(b -> b.getId(), 2); // ❌ λ 表达式,implMethodName 是编译器合成名
qw.eq(new SFunction<Book,Long>(){}); // ❌ 匿名内部类,无 writeReplace延迟失败风险: writeReplace 被 JDK 安全策略阻止(如 --illegal-access=deny)时,且在序列化降级也失败的情况下,异常只在首次调用该方法时抛出,而不是启动时。一个条件查询可能写了大半年没人调,某天才发现它挂了。
链式调用 + 插件体系
链式调用:泛型自引用 Children extends AbstractWrapper<T, R, Children> + typedThis = (Children) this。
插件通过 MyBatis 的 Interceptor 实现:
| 插件 | 拦截点 | 行为 |
|---|---|---|
| 分页 | Executor.query() | 拼接物理分页 SQL |
| 乐观锁 | Executor.update() | 检查版本号 +1 |
| 逻辑删除 | SQL 注入阶段 | DELETE → UPDATE set deleted=1 |
| 多租户 | SQL 解析后 | 追加租户条件 |
| 动态表名 | SQL 构建时 | 运行时替换表名 |
内存分页坑:不配置 PaginationInnerInterceptor 时数据全量查出再截取 → 高数据量 OOM。
多插件顺序问题: 分页 + 多租户 + 动态表名同时启用时,执行顺序靠配置,框架不保证。顺序错误 → SQL 语法/语义错误。需要理解每个插件的拦截阶段。
选型决策
实际项目不二选一,混用。边界:单表用 MP,跨表用 XML。
架构 tradeoff
| 维度 | MP 为主 | MyBatis 为主 |
|---|---|---|
| 开发速度 | 单表 CRUD 零代码 | 每个查询手写 |
| SQL 可控性 | 自动 SQL 需 review | 每行手写,DBA 可审计 |
| 框架耦合 | 代码强依赖 MP API | 纯 MyBatis,可后加增强 |
| 抽象泄漏 | 复杂查询退回 XML | 无(本来就手写) |
| 升级风险 | MP 大版本可能 breaking | MyBatis 稳定 |
MP 适用
- 中后台管理,单表 CRUD >70%
- 字段频繁变更(条件构造器免改 XML)
- 微服务(代码生成器批量产出骨架)
MyBatis XML 适用
- 多表联查、存储过程、报表
- DBA 需审查 SQL(XML 比 Java 字符串拼接好读)
- 金融/交易核心系统
- 存量大量 XML 的项目
演化轨迹
v1 启动期:纯 MyBatis(快速验证,团队熟悉)
v2 增长期:引入 MP,新功能 MP,旧 XML 不动(重复 CRUD 开始堆积)
v3 成熟期:MP 90%,XML 10%,引入代码生成器(团队扩大,需统一规范)
v4 优化期:审视自动 SQL,高频查补手写(自动生成不一定是性能最优)v2→v3 的拐点最容易出问题:当团队发现"既要维护 XML 又要维护条件构造器"时,说明混用边界没划清。在规范里写死:单表 MP,跨表 XML。
决策树
新项目?→ 是 → 默认 MP(复杂 SQL >30% 加 XML)
→ 否 → 大量 XML?→ 是 → 保留 MyBatis,增量 MP
→ 否 → 按新项目评估踩坑记录
- 版本: MP 3.5.x → MyBatis 3.5.x,错配不直接报错但可能
NoSuchMethodError - 分页: 不配插件走内存分页 → OOM
- 条件构造器: 深层嵌套可读性差,调试没法直接拿 SQL,复杂动态条件走 XML
@TableField: 驼峰转下划线一致时不标- 多表: ResultMap 的 association/collection 回 XML
- 插件顺序: 多插件时分页放最后
MP 的设计边界决定了它不会成为 Hibernate 那样的全功能 ORM——没有一级缓存、脏检查、级联持久化。需要这些能力直接上 JPA/Hibernate,别指望 MP 补齐。
它的核心价值在于:随时可以退回到原生 MyBatis。不 fork、不修改、不绑架。
