文章概述
前文衔接
前文《MyBatis 架构全解》《映射器原理》和《插件开发与拦截链实战》分别深入剖析了 MyBatis 原生的 Executor、MapperProxy 和 Interceptor 机制。MyBatis-Plus 正是在这些扩展点之上构建的一套增强框架——它没有修改任何 MyBatis 源码,却通过自定义 SqlSessionFactoryBean、扩展 Interceptor 和动态注入 MappedStatement,为开发者带来了通用 CRUD、分页、乐观锁等开箱即用的能力。本文将正面拆解这些增强机制的底层实现,揭示 MyBatis-Plus 如何成为 MyBatis 扩展点体系的集大成者。
核心观点
从原生 MyBatis 到 MyBatis-Plus,开发者最直观的感受是“连 Mapper XML 都不需要写了”。这一魔法的背后,是 MyBatis-Plus 对 MyBatis 四大对象和扩展点的深度运用:AbstractSqlInjector 动态生成通用的 Insert、Delete、Update、Select 方法对应的 MappedStatement,悄然注入到 Configuration 中;MybatisPlusInterceptor 利用拦截链实现了比原生分页插件更优雅的方言适配;TableInfoHelper 通过解析实体注解构建了类似 JPA 的元数据模型。本文将沿 MyBatis-Plus 的增强架构,逐一拆解这些核心机制的源码实现。
核心要点
- 通用 CRUD 自动注入:
AbstractSqlInjector的扫描、构造与注册机制。 - 分页插件增强:
MybatisPlusInterceptor与PaginationInnerInterceptor的拦截链设计。 - SqlSessionFactory 增强:
MybatisSqlSessionFactoryBean如何整合自动注入。 - 实体元数据解析:
TableInfoHelper对@TableName、@TableId等注解的处理。 - 乐观锁与逻辑删除:通过
Interceptor和SqlInjector实现的透明增强。 - 设计模式:策略、模板方法、工厂、建造者模式的综合应用。
文章组织架构图
flowchart TD
1[1. MyBatis-Plus 增强架构总览: 替换点与扩展点全景]
2[2. 通用 CRUD 的自动注入: AbstractSqlInjector 的源码拆解]
3[3. 分页插件的增强实现: MybatisPlusInterceptor 与方言适配]
4[4. MybatisSqlSessionFactoryBean 的增强整合]
5[5. 实体元数据解析: TableInfoHelper 与注解处理]
6[6. 乐观锁与逻辑删除的透明增强]
7[7. 设计模式总结与原生对比]
8[8. 生产事故排查专题]
9[9. 面试高频专题]
1 --> 2
1 --> 3
1 --> 4
1 --> 5
2 --> 6
3 --> 6
5 --> 6
6 --> 7
7 --> 8
8 --> 9
架构图说明
-
总览说明:全文 9 个模块从 MyBatis-Plus 的增强架构总览出发,逐步深入通用 CRUD 注入、分页增强、工厂增强、元数据解析、乐观锁与逻辑删除,最后通过设计模式总结、事故排查和面试完成闭环。
-
逐模块说明:模块 1 建立 MyBatis-Plus 替换了哪些原生组件的全局认知;模块 2 深入最核心的 SQL 自动注入机制;模块 3-4 剖析分页插件和工厂增强;模块 5 讲解实体元数据的构建;模块 6 展示乐观锁和逻辑删除的透明实现;模块 7 提炼设计模式并与原生 MyBatis 对比;模块 8-9 落地排查与应试。
-
关键结论:MyBatis-Plus 的本质是对 MyBatis 扩展点体系的全面应用——通过替换
SqlSessionFactoryBean整合增强能力,通过动态注入MappedStatement实现零 XML 的 CRUD,通过扩展Interceptor强化分页和乐观锁。理解 MyBatis-Plus 即理解了 MyBatis 扩展点的最大潜力。
1. MyBatis-Plus 增强架构总览:替换点与扩展点全景
MyBatis-Plus 的设计哲学是“只做增强不做改变”,所有增强均通过 MyBatis 开放的扩展点实现,不修改 MyBatis 一行源码。其全局架构围绕三个层面的替换与扩展展开:
- SqlSessionFactory 层:
MybatisSqlSessionFactoryBean替换原生SqlSessionFactoryBean,在构建过程中触发实体元数据解析与 CRUD 方法自动注入。 - Configuration 层:
MybatisConfiguration扩展原生Configuration,增强对 MappedStatement 注册的管理,并添加对自定义 TypeHandler 和逻辑删除的全局支持。 - 插件层:
MybatisPlusInterceptor作为新一代多拦截器链,统一管理分页、乐观锁、多租户等内部拦截器,完全兼容原生Interceptor接口。
这些替换组件与原生组件的对应关系以及它们利用的扩展点如下全景类图所示。
classDiagram
class SqlSessionFactoryBean {
+buildSqlSessionFactory() SqlSessionFactory
}
class MybatisSqlSessionFactoryBean {
+afterPropertiesSet()
+getObject() SqlSessionFactory
}
MybatisSqlSessionFactoryBean --|> SqlSessionFactoryBean : 继承
class Configuration {
+MappedStatement getMappedStatement(String id)
+void addMappedStatement(MappedStatement ms)
}
class MybatisConfiguration {
+void addMappedStatement(MappedStatement ms)
+void addInterceptor(Interceptor interceptor)
}
MybatisConfiguration --|> Configuration : 继承
class Interceptor {
+intercept(Invocation invocation) Object
}
class MybatisPlusInterceptor {
+intercept(Invocation invocation) Object
+addInnerInterceptor(InnerInterceptor inner)
}
MybatisPlusInterceptor ..|> Interceptor : 实现
class InnerInterceptor {
+beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
+beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout)
}
class AbstractSqlInjector {
+inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass)
}
class DefaultSqlInjector {
+getMethodList(Class<?> mapperClass, TableInfo tableInfo) List<AbstractMethod>
}
AbstractSqlInjector --> MappedStatement : 动态构造并注册
AbstractSqlInjector --> TableInfo : 读取表元数据
class TableInfoHelper {
-static Map<String, TableInfo> tableInfoMap
+initTableInfo(MapperBuilderAssistant assistant, Class<?> clazz) TableInfo
}
TableInfoHelper --> TableInfo : 构建缓存
class TableInfo {
+String tableName
+String keyColumn
+List<TableFieldInfo> fieldList
}
图表主旨概括:展示 MyBatis-Plus 如何通过继承和实现原生组件,在 SqlSessionFactory、Configuration、Interceptor 三大核心层面植入增强逻辑,同时利用独立组件完成元数据解析和 SQL 动态注入。
逐层/逐元素分解:
MybatisSqlSessionFactoryBean继承原生SqlSessionFactoryBean,在 Spring 容器初始化时通过afterPropertiesSet()方法劫持工厂构建流程,触发TableInfoHelper的全局扫描,最终生成一个包含自动 SQL 能力的SqlSessionFactory。MybatisConfiguration继承原生Configuration,重写addMappedStatement()方法以确保动态注入的MappedStatement能够正确注册,并与业务手写 SQL 共存。MybatisPlusInterceptor实现原生Interceptor接口,但内部维护了一个InnerInterceptor列表,通过组合方式将分页、乐观锁等拦截逻辑拆分为内聚的拦截器单元,解决了原生多拦截器排序难的问题。AbstractSqlInjector作为 SQL 注入的核心抽象,依赖TableInfoHelper提供的实体元数据,动态构造MappedStatement并注册到Configuration的mappedStatements集合中。
设计原理映射:该架构广泛运用了模板方法模式(AbstractSqlInjector 定义注入骨架)、工厂模式(MybatisSqlSessionFactoryBean 作为增强工厂)、组合模式(MybatisPlusInterceptor 组合多个 InnerInterceptor)和策略模式(内部拦截器的不同实现)。
工程联系与关键结论:MyBatis-Plus 并非另起炉灶,而是精准识别了 MyBatis 设计中的关键扩展点(SqlSessionFactory 构建、MappedStatement 注册、拦截器链),通过继承和多态进行无侵入增强。理解这一架构是精通 MyBatis-Plus 的基础。
2. 通用 CRUD 的自动注入:AbstractSqlInjector 的源码拆解
通用 CRUD 是 MyBatis-Plus 最核心的增强能力。它的实现完全依赖 MyBatis 的 MappedStatement 动态注册机制:对每一个 Mapper 接口,MyBatis-Plus 会动态生成对应的 Insert、Delete、Update、SelectList 等方法的 MappedStatement,并将其注入到全局 Configuration 中。这个过程由 AbstractSqlInjector 及其子类 DefaultSqlInjector 完成。
2.1 AbstractSqlInjector.inspectInject 的扫描与注入流程
AbstractSqlInjector.inspectInject() 是通用 CRUD 注入的入口方法。当 Mapper 接口被 MyBatis 扫描并注册时,MyBatis-Plus 会调用此方法,为每个 Mapper 检查并注入通用方法。
关键源码(com.baomidou.mybatisplus.core.injector.AbstractSqlInjector):
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
// 1. 获取实体类 Class
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
// 2. 解析实体,构建 TableInfo
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 3. 获取要注入的方法列表(模板方法)
List<AbstractMethod> methodList = getMethodList(mapperClass, tableInfo);
if (CollectionUtils.isNotEmpty(methodList)) {
// 4. 遍历注入每一个方法对应的 MappedStatement
for (AbstractMethod method : methodList) {
method.inject(builderAssistant, mapperClass, modelClass, tableInfo);
}
}
}
}
解读:
extractModelClass通过 Mapper 的泛型签名解析出实体类型。- 调用
TableInfoHelper.initTableInfo解析实体注解,构建TableInfo元数据对象。 getMethodList是抽象方法,由子类实现,返回包含Insert、Delete、Update、SelectById等AbstractMethod子类的列表。这是模板方法模式的核心:骨架流程固定,方法列表的具体构成由子类决定。- 遍历
AbstractMethod列表,调用inject方法完成具体MappedStatement的构造和注册。
2.2 AbstractMethod.inject 构造 MappedStatement
每个具体的 AbstractMethod(如 Insert、Delete)负责生成一条特定的 SQL 模板,并借助 MappedStatement.Builder 构造出完整的 MappedStatement。
以 Insert 方法为例(com.baomidou.mybatisplus.core.injector.methods.Insert):
public class Insert extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 拼接 SQL: INSERT INTO table (columns) VALUES (#{property1}, #{property2} ...)
String sql = String.format(SqlMethod.INSERT_ONE.getSql(),
tableInfo.getTableName(),
tableInfo.getKeyColumn(),
tableInfo.getAllSqlSelect()); // 所有字段
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 构造 MappedStatement
return this.addInsertMappedStatement(mapperClass, modelClass, SqlMethod.INSERT_ONE.getMethod(), sqlSource);
}
}
解读:
SqlMethod.INSERT_ONE是预定义的 SQL 模板,形如INSERT INTO %s (%s) VALUES (%s)。tableInfo.getAllSqlSelect()返回实体所有列名拼接的字符串。languageDriver.createSqlSource会基于 SQL 字符串和实体类型创建SqlSource。默认XMLLanguageDriver会将#{}参数识别为动态参数,从而生成StaticSqlSource(因为 SQL 是静态文本,没有 OGNL 动态标签)。addInsertMappedStatement最终调用MappedStatement.Builder构建对象并注册到Configuration。
内联示例:观察自动 CRUD 的 SQL 输出
// 实体类
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private Integer age;
// getter/setter...
}
// Mapper 接口
public interface UserMapper extends BaseMapper<User> {
}
// 测试代码
@SpringBootTest
public class AutoCrudTest {
@Autowired
private UserMapper userMapper;
@Test
public void testInsert() {
User user = new User();
user.setUsername("zhangsan");
user.setAge(25);
userMapper.insert(user); // 自动注入的 Insert 方法
}
}
控制台日志输出(MyBatis-Plus 配置 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl):
==> Preparing: INSERT INTO user ( username, age ) VALUES ( ?, ? )
==> Parameters: zhangsan(String), 25(Integer)
关键结论:AbstractSqlInjector 将实体映射信息转化为标准 MappedStatement,并注入到原生 Configuration 中。整个过程完全遵循 MyBatis 的 SQL 执行流程,业务手写的 XML 或注解 SQL 不会受到任何影响。
2.3 通用 CRUD 注入的序列图
sequenceDiagram
participant Spring as Spring 容器
participant MSSFB as MybatisSqlSessionFactoryBean
participant SFB as SqlSessionFactoryBean
participant MBA as MapperBuilderAssistant
participant ASI as AbstractSqlInjector
participant TH as TableInfoHelper
participant AM as AbstractMethod (Insert)
participant CFG as MybatisConfiguration
Spring ->> MSSFB: afterPropertiesSet()
MSSFB ->> SFB: super.buildSqlSessionFactory()
loop 扫描每个 Mapper 接口
SFB ->> MBA: 处理 mapperClass
MSSFB ->> ASI: inspectInject(builderAssistant, mapperClass)
ASI ->> TH: initTableInfo(modelClass)
TH -->> ASI: TableInfo
ASI ->> AM: inject(builderAssistant, mapperClass, modelClass, tableInfo)
AM ->> AM: 构造 SqlSource (INSERT INTO user (...) VALUES (...))
AM ->> CFG: addMappedStatement(ms)
end
MSSFB -->> Spring: SqlSessionFactory
图表主旨概括:展示 Spring 容器初始化 MyBatis-Plus 时,如何从 MybatisSqlSessionFactoryBean 出发,串行触发实体元数据解析和通用 SQL 方法注入的全过程。
逐层/逐元素分解:
- Spring 调用
MybatisSqlSessionFactoryBean.afterPropertiesSet(),该方法首先调用父类SqlSessionFactoryBean.buildSqlSessionFactory()完成原生 MyBatis 的解析和工厂构建。 - 在解析每个 Mapper 接口时,MyBatis-Plus 劫持
MapperBuilderAssistant的处理流程,将 Mapper 接口传递给AbstractSqlInjector.inspectInject。 inspectInject内部先通过TableInfoHelper获取表元数据,再将所有AbstractMethod子类实例依次执行inject,构造并注册MappedStatement。
设计原理映射:AbstractSqlInjector 是典型的模板方法模式骨架,getMethodList() 交由子类实现具体注入方法集合;AbstractMethod 的各个子类通过建造者模式(MappedStatement.Builder)一步一步构建出完整的映射语句。
工程联系与关键结论:自动注入完全依托 MyBatis 标准的 MappedStatement 注册通道,注入的 MappedStatement 与 XML 中手动定义的没有本质区别。这意味着它们共享相同的缓存、插件拦截和 SQL 执行器,不会破坏 MyBatis 的内核一致性。
3. 分页插件的增强实现:MybatisPlusInterceptor 与方言适配
分页是 MyBatis-Plus 最常用的增强功能之一。其实现基于 MyBatis 的拦截器机制,但相比于原生分页插件的单一拦截逻辑,MyBatis-Plus 从 3.4.0 开始引入新一代 MybatisPlusInterceptor,将单个拦截器拆分为“主拦截器 + 内部拦截器链”的架构,使得分页、乐观锁、多租户等功能能够按需组合且执行顺序可控。
3.1 MybatisPlusInterceptor 的多拦截器链设计
MybatisPlusInterceptor 实现了 org.apache.ibatis.plugin.Interceptor 接口,但其核心是维护一个 List<InnerInterceptor> 集合,所有内部拦截器按添加顺序执行。
关键源码(com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor):
public class MybatisPlusInterceptor implements Interceptor {
private List<InnerInterceptor> interceptors = new ArrayList<>();
public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
this.interceptors.add(innerInterceptor);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor) {
for (InnerInterceptor interceptor : interceptors) {
interceptor.beforeQuery((Executor) target, (MappedStatement) args[0], args[1],
(RowBounds) args[2], (ResultHandler) args[3], (BoundSql) args[4]);
}
Object result = invocation.proceed();
for (int i = interceptors.size() - 1; i >= 0; i--) {
interceptors.get(i).afterQuery(...);
}
return result;
}
// ... 类似处理 StatementHandler、ParameterHandler、ResultSetHandler
return invocation.proceed();
}
}
解读:
InnerInterceptor接口定义了beforeQuery、beforePrepare、beforeUpdate等生命周期回调方法,覆盖了 Executor、StatementHandler 等关键拦截点。MybatisPlusInterceptor通过invocation.getTarget()判断当前拦截的目标类型,分别执行相应的内部拦截器链。- 这种“集中入口 + 责任链”的设计解决了原生 MyBatis 多插件通过
@Intercepts注解排序困难的问题,用户只需按顺序添加内部拦截器即可精确控制执行顺序。
3.2 PaginationInnerInterceptor 的方言适配与 BoundSql 改写
PaginationInnerInterceptor 是负责分页的核心内部拦截器,它拦截 Executor.query 或 StatementHandler.prepare,根据数据库方言和分页参数动态改写 SQL 并执行 count 查询。
执行流程(详见 beforeQuery 方法):
- 判断是否需要分页:检查传入参数是否为
IPage子类,且searchCount为true(默认开启)。 - 执行 count 查询:根据原始 SQL 构建 count 语句,通过
jsqlparser优化(如移除不必要的ORDER BY),然后调用Executor.query获取总记录数。 - 拼接分页 SQL:使用方言实现类(如
MySQLDialect)在原始 SQL 后追加LIMIT offset, limit。 - 改写 BoundSql:生成新的
BoundSql对象,替换原有 SQL 和参数映射,并传递给后续执行器。
分页 SQL 改写源码片段(PaginationInnerInterceptor.beforeQuery 及相关辅助方法):
protected void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (page == null || page.getSize() < 0 || !page.searchCount()) {
return;
}
// count 查询
long total = queryCount(executor, ms, parameter, boundSql);
page.setTotal(total);
// 分页 SQL
String originalSql = boundSql.getSql();
String pageSql = dialect.buildPaginationSql(originalSql, page.offset(), page.getSize());
// 覆盖 BoundSql
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pageSql,
boundSql.getParameterMappings(), parameter);
// 通过反射将 newBoundSql 设置回 BoundSql 关联的 StatementHandler
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", pageSql);
}
解读:
dialect.buildPaginationSql是策略模式的体现,针对 MySQL、PostgreSQL、Oracle 等不同数据库有不同的方言实现。- count 查询默认会通过
JSqlParser进行优化,例如去除ORDER BY、去掉不必要的列等,这可以通过optimizeCountSql选项控制。 - 改写后的
BoundSql直接替换了原对象的关键属性,由于后续StatementHandler使用的就是该BoundSql实例,因此实际执行的 SQL 已经被修改。
配置示例与验证:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setOptimizeCountSql(true); // 开启 count 优化
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
测试分页查询:
Page<User> page = new Page<>(2, 10);
userMapper.selectPage(page, Wrappers.<User>lambdaQuery().gt(User::getAge, 18));
System.out.println("总记录数:" + page.getTotal());
System.out.println("当前页记录:" + page.getRecords().size());
日志输出:
==> Preparing: SELECT COUNT(*) FROM user WHERE age > ?
==> Preparing: SELECT id,username,age FROM user WHERE age > ? LIMIT 10,10
关键结论:MybatisPlusInterceptor 把复杂的拦截逻辑切分为独立的 InnerInterceptor,实现了对 MyBatis 拦截器机制的再造升级。分页插件通过在 Executor 层拦截并改写 BoundSql,完全复用 MyBatis 原生的 SQL 执行引擎。
3.3 分页拦截器改写 BoundSql 序列图
sequenceDiagram
participant Executor as MyBatis Executor
participant MPI as MybatisPlusInterceptor.intercept
participant PII as PaginationInnerInterceptor
participant Dialect as JdbcDialect (MySQL)
participant BoundSql as BoundSql
participant ExecutorQ as Executor.query (count)
Executor ->> MPI: intercept (target=Executor)
MPI ->> PII: beforeQuery(executor, ms, param, rowBounds, boundSql)
PII ->> PII: 检查参数存在 IPage && searchCount=true
PII ->> Dialect: buildCountSql(originalSql)
Dialect -->> PII: countSql
PII ->> ExecutorQ: executor.query(ms, param, rowBounds, countBoundSql)
ExecutorQ -->> PII: totalCount
PII ->> page: setTotal(totalCount)
PII ->> Dialect: buildPaginationSql(originalSql, offset, limit)
Dialect -->> PII: pageSql
PII ->> BoundSql: 反射修改 sql 为 pageSql
PII -->> MPI: 返回
MPI ->> Executor: 继续执行 (invocation.proceed)
Executor ->> BoundSql: 执行改写后的 SQL
图表主旨概括:完整演示分页拦截器如何在 MyBatis 执行器层面截获查询操作,执行 count 查询并改写原始 SQL,最终将分页 SQL 送回原执行流程。
逐层/逐元素分解:
MybatisPlusInterceptor收到Executor.query调用后,依次调用内部拦截器的beforeQuery。PaginationInnerInterceptor提取IPage参数,决定是否进行分页处理。- 使用
dialect构建 count SQL 并执行一次单独的查询,将结果写入page.setTotal。 - 再次使用
dialect构建分页 SQL,并通过 MyBatis 的MetaObject反射机制直接修改BoundSql的sql字段,使其指向分页 SQL。 invocation.proceed()继续执行,此时Executor拿到的是更改后的BoundSql,实际对数据库执行分页查询。
设计原理映射:JdbcDialect 接口及 MySQLDialect、OracleDialect 等实现构成策略模式,用于屏蔽不同数据库的分页语法差异;MybatisPlusInterceptor 采用组合模式管理多个内部拦截器,是对责任链模式的灵活实现。
工程联系与关键结论:分页插件不依赖 XML 或注解配置,完全通过拦截器透明织入。这种设计启示我们,任何需要统一修改 SQL 或干预执行流程的需求(如多租户字段填充、数据权限过滤)都可以借鉴这种“Executor 层拦截 + BoundSql 改写”的模式。
4. MybatisSqlSessionFactoryBean 的增强整合
MybatisSqlSessionFactoryBean 是 MyBatis-Plus 整合 Spring Boot 的枢纽,它继承原生的 SqlSessionFactoryBean,在工厂构建的关键节点插入增强逻辑,从而使自动 CRUD 注入、实体元数据解析等能力无缝融入 MyBatis 的生命周期。
4.1 类结构与增强点
MybatisSqlSessionFactoryBean 通过重写 afterPropertiesSet() 和 getObject() 方法,在父类构建完成 SqlSessionFactory 后触发一系列后置处理。
核心源码骨架(com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean):
public class MybatisSqlSessionFactoryBean extends SqlSessionFactoryBean {
@Override
public void afterPropertiesSet() throws Exception {
// 1. 调用父类构建原生 SqlSessionFactory
super.afterPropertiesSet();
// 2. 获取 Configuration(此时为 MybatisConfiguration 实例)
MybatisConfiguration configuration = (MybatisConfiguration) getSqlSessionFactory().getConfiguration();
// 3. 初始化全局配置(如枚举处理、SQLLogger 等)
// 4. 启用 TableInfoHelper 全局初始化钩子
// 此步骤并非简单扫描,实际注入在 XML 映射解析阶段触发
}
}
实际上,自动注入并非在 afterPropertiesSet 中一次性完成所有扫描,而是通过 MyBatis 的 XMLMapperBuilder 解析 Mapper XML 或 MapperAnnotationBuilder 解析注解接口时,MP 劫持了 MapperBuilderAssistant,进而触发 AbstractSqlInjector.inspectInject。在 Spring Boot 自动配置中,MybatisPlusAutoConfiguration 通过 MapperScannerConfigurer 提前扫描 Mapper 包,并在构建 SqlSessionFactory 时确保 Mapper 接口与 XML 一同被解析。
4.2 初始化与自动注入触发序列图
sequenceDiagram
autonumber
participant Spring as Spring Boot 启动
participant AutoConf as MybatisPlusAutoConfiguration
participant MSSFB as MybatisSqlSessionFactoryBean
participant SFB as SqlSessionFactoryBean
participant CFG as MybatisConfiguration
participant MapperReg as MapperRegistry
participant MBA as MapperBuilderAssistant
participant Injector as AbstractSqlInjector
Spring ->> AutoConf: 自动配置
AutoConf ->> MSSFB: 初始化并调用 afterPropertiesSet
MSSFB ->> SFB: buildSqlSessionFactory()
SFB ->> CFG: 创建 Configuration
loop 扫描每个 Mapper 接口
SFB ->> MapperReg: addMapper(mapperInterface)
MapperReg ->> MBA: 创建 MapperAnnotationBuilder
MBA ->> MSSFB: (钩子) 触发 MP 增强处理
MSSFB ->> Injector: "inspectInject(mapperAssistant, mapperClass)"
end
MSSFB -->> Spring: SqlSessionFactory 就绪
图表主旨概括:描述 Spring Boot 集成 MyBatis-Plus 时,MybatisSqlSessionFactoryBean 如何在原生工厂构建过程中注入自动 CRUD 能力。
逐层/逐元素分解:
MybatisPlusAutoConfiguration使用@MapperScan注册的 Mapper 在构建SqlSessionFactory时被一一解析。- 原生
MapperRegistry.addMapper会创建MapperAnnotationBuilder并遍历接口方法解析,但对于BaseMapper继承的方法,原生无法处理。 - MyBatis-Plus 替换了
MapperAnnotationBuilder的处理逻辑(通过MybatisMapperAnnotationBuilder),在处理 Mapper 时额外调用AbstractSqlInjector.inspectInject,从而将通用 CRUD 方法注入进去。 - 这一过程完全透明,业务代码无需感知。
设计原理映射:MybatisSqlSessionFactoryBean 是典型的装饰器模式/工厂方法模式应用,它在不改变父类接口的前提下对构建过程进行了增强。
工程联系与关键结论:对 SqlSessionFactoryBean 的替换是 MyBatis-Plus 整合能力的基础。开发者若要开发自己的 MyBatis 增强框架,首要步骤就是提供自定义的 SqlSessionFactoryBean,以获得对工厂构建过程的完全控制权。
5. 实体元数据解析:TableInfoHelper 与注解处理
实体元数据是 MyBatis-Plus 各模块运行的基础。通用 CRUD 注入需要知道表名、主键列、字段映射;分页插件处理逻辑删除时需要知道逻辑删除字段;乐观锁需要识别版本字段。所有这些信息均通过 TableInfoHelper 解析实体注解并缓存为 TableInfo 对象。
5.1 TableInfoHelper.initTableInfo 的解析逻辑
TableInfoHelper 内部维护了一个静态 ConcurrentHashMap,存放每个实体类与 TableInfo 的映射关系。当某个 Mapper 接口首次被处理时,会触发对应实体的解析。
核心源码(com.baomidou.mybatisplus.core.toolkit.TableInfoHelper):
public static synchronized TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
String cacheKey = builderAssistant.getCurrentNamespace() + ":" + clazz.getName();
TableInfo tableInfo = TABLE_INFO_CACHE.get(cacheKey);
if (tableInfo == null) {
tableInfo = new TableInfo(clazz);
// 解析 @TableName 注解
TableName table = clazz.getAnnotation(TableName.class);
String tableName = table == null ? StringPool.EMPTY : table.value();
// ... 自动驼峰转下划线
tableInfo.setTableName(tableName);
// 解析 @TableId
List<Field> allFields = ReflectionKit.getFieldList(clazz);
for (Field field : allFields) {
TableId tableId = field.getAnnotation(TableId.class);
if (tableId != null) {
tableInfo.setKeyColumn(columnName);
tableInfo.setKeyProperty(field.getName());
tableInfo.setKeyType(tableId.type());
}
// 解析 @TableField
TableField tableField = field.getAnnotation(TableField.class);
if (tableField != null && tableField.exist()) {
TableFieldInfo fieldInfo = new TableFieldInfo(field, tableField);
tableInfo.getFieldList().add(fieldInfo);
}
}
// 缓存
TABLE_INFO_CACHE.put(cacheKey, tableInfo);
}
return tableInfo;
}
解读:
- 方法使用同步锁保证线程安全,确保一个实体类仅解析一次。
TableName注解可指定表名,若不指定则按驼峰转下划线规则生成。- 遍历所有字段,识别
@TableId确定主键及 ID 生成策略(AUTO、INPUT、ASSIGN_ID等)。 @TableField注解标记普通列,可配置映射列名、是否参与插入/更新、是否为逻辑删除字段等。- 解析后的
TableInfo包含了keyColumn、keyProperty、fieldList等完整信息,供 SQL 注入和拦截器使用。
5.2 实体注解解析序列图
sequenceDiagram
participant MBA as MapperBuilderAssistant
participant TH as TableInfoHelper
participant Cache as TABLE_INFO_CACHE
participant Class as User.class
participant TI as TableInfo
participant Field as Field: id/username/age
MBA ->> TH: initTableInfo(assistant, User.class)
TH ->> Cache: 检查 User.class 是否已有缓存
Cache -->> TH: null (未缓存)
TH ->> TI: new TableInfo(User.class)
TH ->> Class: 获取 @TableName 注解
Class -->> TH: 表名 "user"
TI ->> TI: setTableName("user")
loop 遍历所有字段
TH ->> Field: 获取 @TableId 注解
Field -->> TH: 主键字段 id
TI ->> TI: setKeyColumn("id"), setKeyProperty("id")
TH ->> Field: 获取 @TableField 注解
Field -->> TH: 普通字段信息
TI ->> TI: addFieldInfo(TableFieldInfo)
end
TH ->> Cache: 放入缓存
TH -->> MBA: TableInfo 实例
图表主旨概括:展示 TableInfoHelper 如何按需解析实体类的 JPA 风格注解,构建一个包含完整映射信息的 TableInfo 对象并缓存。
逐层/逐元素分解:
- 每当
MapperBuilderAssistant请求实体元数据时,TableInfoHelper先查询缓存。 - 缓存未命中时,实例化
TableInfo,反射读取@TableName、@TableId、@TableField等注解。 - 构建完成的
TableInfo放入ConcurrentHashMap,后续所有对该实体的访问都会直接命中缓存。
设计原理映射:采用单例缓存模式,确保每个实体只解析一次;使用反射进行元数据提取,属于内省模式的实践。
工程联系与关键结论:TableInfoHelper 是 MyBatis-Plus 的“元数据中心”。任何依赖实体-表映射信息的增强组件(如 SQL 注入器、拦截器)都应通过它获取结构化的元数据,而不是各自去解析注解,这是保证一致性和性能的关键设计。
6. 乐观锁与逻辑删除的透明增强
MyBatis-Plus 的乐观锁和逻辑删除分别体现了对 MyBatis Interceptor 和 SqlInjector 扩展点的进一步运用,实现了无感知的功能增强。
6.1 乐观锁实现原理
乐观锁通过 OptimisticLockerInnerInterceptor 实现,拦截 Executor.update 方法,在执行 UPDATE 语句前自动追加版本号字段的 WHERE 条件和 SET 更新。
使用要求:实体类中定义一个整数类型版本字段,并标注 @Version 注解。
public class User {
@TableId
private Long id;
private String username;
@Version
private Integer version;
}
乐观锁拦截器核心源码(OptimisticLockerInnerInterceptor.beforeUpdate):
protected void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {
if (parameter instanceof Map) {
Map<String, Object> map = (Map<String, Object>) parameter;
// 识别实体对象中带有 @Version 字段
Object entity = map.getOrDefault("et", map.get("param1"));
if (entity != null) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
if (tableInfo != null && tableInfo.isWithVersion()) {
String versionColumn = tableInfo.getVersionColumn();
Object versionValue = tableInfo.getVersionValue(entity);
// 在原 SQL 上追加 WHERE version = #{versionValue} AND SET version = version + 1
String originalSql = ms.getBoundSql(parameter).getSql();
String newSql = addVersionToUpdate(originalSql, versionColumn, versionValue);
// 修改 BoundSql 和 MappedStatement 的 SqlSource
// ...类似分页插件的 BoundSql 改写
}
}
}
}
执行示例:执行 userMapper.updateById(user),假设 user.version = 1:
原生 SQL 本为:
UPDATE user SET username = ? WHERE id = ?
拦截后变为:
UPDATE user SET username = ?, version = version + 1 WHERE id = ? AND version = ?
参数:username、id、version(1)。
若更新影响 0 行,说明版本号已被其他事务修改,MyBatis-Plus 会抛出 OptimisticLockException(取决于配置)。
内联验证:
@Test
public void testOptimisticLock() {
User user = userMapper.selectById(1L);
user.setUsername("lisi");
userMapper.updateById(user); // version 自动 +1,WHERE version = oldVersion
}
日志:
UPDATE user SET username=?, version=version+1 WHERE id=? AND version=?
关键点:乐观锁的实现完全依赖拦截器对 BoundSql 的修改,不侵入业务 SQL 编写。
6.2 逻辑删除实现原理
逻辑删除通过 @TableLogic 注解标记字段,并由 SqlInjector 改写通用删除、查询操作的 SQL 模板。
实体定义:
@TableName("user")
public class User {
@TableId
private Long id;
@TableLogic(value = "0", delval = "1")
private Integer deleted;
}
MyBatis-Plus 的 DefaultSqlInjector 会根据 TableInfo 中是否存在逻辑删除字段,自动调整 SQL 行为:
- 删除操作:
DELETE FROM user WHERE id = ?被改写为UPDATE user SET deleted = 1 WHERE id = ? AND deleted = 0。 - 查询/更新操作:所有
SELECT或UPDATE语句自动在WHERE中追加AND deleted = 0。
这种改写不是在拦截器中动态修改 BoundSql,而是在注入阶段就构造了包含逻辑删除条件的 SqlSource。例如 SelectById 的 SQL 模板会被拼接为:
SELECT id,username,deleted FROM user WHERE id = #{id} AND deleted = 0
因此注册的 MappedStatement 本身就带有逻辑删除条件,性能更好且无运行时开销。
逻辑删除注入源码片段(com.baomidou.mybatisplus.core.injector.methods.DeleteById 条件拼接):
String logicSql = tableInfo.getLogicDeleteSql(true, true);
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
tableInfo.getKeyColumn(), logicSql);
// 其中 logicSql = " AND deleted = 0 " (条件) 或 " SET deleted = 1 " (删除转换)
验证:
userMapper.deleteById(1L);
输出 SQL:
UPDATE user SET deleted=1 WHERE id=? AND deleted=0
查询时自动追加:
SELECT id,username,deleted FROM user WHERE deleted=0
关键结论:乐观锁采用拦截器运行时改写,逻辑删除采用 SQL 注入时静态改写。两种策略的选择取决于增强行为的时机:乐观锁需要在每次执行时动态注入版本号值,而逻辑删除的条件是静态的,提前在 SQL 模板中写死更为高效。
7. 设计模式总结与原生对比
7.1 设计模式提炼
MyBatis-Plus 的增强架构中大量运用了经典设计模式,梳理如下:
- 策略模式:不同数据库方言的分页处理(
JdbcDialect接口与MySQLDialect、OracleDialect等具体实现);主键生成策略(IdType.AUTO、ASSIGN_ID等)。 - 模板方法模式:
AbstractSqlInjector.inspectInject定义扫描、解析、注入骨架,子类DefaultSqlInjector通过getMethodList()决定具体注入哪些方法;AbstractMethod.inject负责构造MappedStatement。 - 工厂模式:
MybatisSqlSessionFactoryBean作为增强的SqlSessionFactory工厂;MybatisPlusAutoConfiguration根据配置自动组装。 - 建造者模式:
MappedStatement.Builder被各类AbstractMethod子类用于逐步构建映射语句对象。 - 组合模式/责任链模式:
MybatisPlusInterceptor组合多个InnerInterceptor,形成可定制的拦截器链。 - 单例与享元模式:
TableInfoHelper全局缓存实体元数据,避免重复解析。
7.2 与原生 MyBatis 的对比
| 能力维度 | MyBatis 原生 | MyBatis-Plus 增强 |
|---|---|---|
| CRUD SQL | 需手动编写 XML 或注解 SQL | 通用 CRUD 自动注入,零配置 |
| 分页 | 自行编写插件或使用第三方 | 内置 MybatisPlusInterceptor,方言自动适配,count 查询自动优化 |
| 乐观锁 | 需手工在 SQL 中加入 version 条件 | 添加 @Version 注解即可,拦截器自动改写 |
| 逻辑删除 | 手工修改所有 SQL 添加 deleted 条件 | @TableLogic 注解即可,SQL 注入期自动拼接条件 |
| 实体映射 | ResultMap 手动配置 | @TableName、@TableId、@TableField 注解驱动,自动驼峰映射 |
| 插件管理 | 单一 @Intercepts 定义,顺序难控 | MybatisPlusInterceptor 内部拦截器链,顺序精准控制 |
| 扩展方式 | 编写自定义插件、TypeHandler | 可基于 AbstractSqlInjector、InnerInterceptor、TableInfoHelper 等快速扩展 |
核心区别感知:原生 MyBatis 给予开发者理论上无限的灵活性,但缺乏开箱即用的生产力增强。MyBatis-Plus 在原生扩展点之上提供了层次化的增强组件,开发者既可以完全遵循 MyBatis 标准进行深入定制,又可以直接享受 90% 常见场景的自动化。
8. 生产事故排查专题
8.1 案例一:逻辑删除字段未加索引导致全表扫描雪崩
现象:某生产环境在业务高峰期频繁出现慢查询告警,数据库 CPU 飙升到 100%。排查发现大量查询都带有 WHERE deleted = 0 条件,但 deleted 字段没有索引。
排查过程:
- 通过
SHOW FULL PROCESSLIST发现大量SELECT ... FROM order WHERE deleted = 0状态为Sending data。 - 查看慢查询日志,该 SQL 平均扫描行数达 500 万,rows examined 与表记录数一致,说明为全表扫描。
- 检查表结构,
deleted字段为tinyint,无任何索引。 - 确认实体类使用了
@TableLogic,MyBatis-Plus 自动为所有查询追加了AND deleted = 0。
根因:MyBatis-Plus 的逻辑删除机制透明添加 deleted 条件,但开发者忽略了该字段的选择性,未创建索引。当表数据量增大后,全表扫描导致磁盘 I/O 瓶颈。
解决:
- 立即为
deleted字段添加索引:ALTER TABLE order ADD INDEX idx_deleted (deleted); - 同时考虑复合索引,将查询高频的
user_id + deleted建立联合索引。
最佳实践:
- 逻辑删除字段必须添加索引,尤其在数据量较大的表上。
- 在项目初期就应规划好逻辑删除字段的索引设计,避免后期被动。
- 监控慢查询,对自动追加的条件保持敏感。
8.2 案例二:分页 count 查询因复杂子查询导致性能雪崩
现象:某报表查询接口返回数据超时,监控显示 count 查询耗时超过 15 秒,而数据查询仅需 0.5 秒。
排查:
- 通过 MyBatis-Plus 日志发现 count SQL 长这样:
SELECT COUNT(*) FROM (SELECT a.*, b.name FROM order a LEFT JOIN user b ON a.user_id=b.id WHERE ...) tmp_count - 原因:业务 XML 中使用了复杂的左连接查询,MyBatis-Plus 的
optimizeCountSql优化原本只移除了ORDER BY,但JSqlParser未能优化掉不必要的左连接,导致 count 实际是全表扫描子查询结果。 - 查看
PaginationInnerInterceptor配置,optimizeCountSql为true(默认),但优化能力有限。
根因:JSqlParser 对非常复杂的 SQL 优化可能不彻底,甚至改变语义,但该案例中优化后的 count 仍保留了不必要的外部连接,性能极差。
解决:
- 临时关闭
optimizeCountSql,并自定义 count 查询:在业务层手动调用page.setSearchCount(false),然后单独编写轻量 count SQL 执行后page.setTotal(count)。 - 或者重写
PaginationInnerInterceptor.dialect.buildPaginationSql进行针对性优化。
最佳实践:
- 对于复杂关联查询,不要依赖自动 count 优化,应手动编写优化的 count SQL。
- 在分页查询中,若 count 不是必须(如前端滚动加载),可设置
searchCount = false减少一次查询。 - 定期审查慢 SQL,对自动生成的 count 语句进行评估。
事故排查序列图:
sequenceDiagram
participant App as 应用服务
participant MPI as MybatisPlusInterceptor
participant Parser as JSqlParser
participant DB as 数据库
App ->> MPI: 分页查询复杂报表 page(current, size)
MPI ->> Parser: 优化原始 SQL 生成 count SQL
Parser -->> MPI: 优化不充分,保留 JOIN
MPI ->> DB: 执行 count SQL (包含大表 JOIN)
DB -->> MPI: 耗时15s,CPU飙升
MPI ->> MPI: 继续分页 SQL 执行 (快速)
MPI -->> App: 分页结果,但总耗时过长
Note over App,DB: 根因:自动 count 优化对复杂 SQL 无效
Note over App,DB: 解决:手动编写 count 或关闭自动 count
9. 面试高频专题
-
MyBatis-Plus 如何在不修改 MyBatis 源码的前提下实现通用 CRUD?
一句话回答:通过替换
SqlSessionFactoryBean为MybatisSqlSessionFactoryBean,在 Mapper 接口解析阶段利用AbstractSqlInjector动态构造MappedStatement并注册到Configuration中,完全遵循 MyBatis 原生扩展点机制。详细解释:MyBatis 在构建
SqlSessionFactory时会解析每个 Mapper 接口或 XML 文件,生成MappedStatement并存入Configuration.mappedStatements。MybatisSqlSessionFactoryBean继承原生的SqlSessionFactoryBean,重写了构建过程的关键步骤:当MapperRegistry调用addMapper时,MyBatis-Plus 劫持了MapperBuilderAssistant的流程,针对每个 Mapper 接口(尤其是继承BaseMapper的接口)触发AbstractSqlInjector.inspectInject。该方法通过TableInfoHelper获取实体的表名、主键、字段映射等元数据,然后调用DefaultSqlInjector.getMethodList获取需要注入的方法列表(如Insert、Delete、Update、SelectById等AbstractMethod子类)。每个AbstractMethod内部使用MappedStatement.Builder一步一步构造出完整的MappedStatement,最终调用Configuration.addMappedStatement注册,与手写 SQL 的MappedStatement平等共存。所以整个过程没有修改 MyBatis 源码,只是利用了其开放的扩展点。 -
AbstractSqlInjector与DefaultSqlInjector的关系?一句话回答:
AbstractSqlInjector定义了自动注入的模板骨架,DefaultSqlInjector实现了getMethodList()方法,返回包含标准 CRUD 操作的AbstractMethod列表。详细解释:
AbstractSqlInjector是典型的模板方法模式实现。它提供了一个不可被覆盖的inspectInject方法,其骨架流程包括:提取实体类 → 调用TableInfoHelper.initTableInfo解析元数据 → 调用抽象的getMethodList()获取方法集合 → 遍历每个方法执行inject注册。getMethodList()被推迟到子类实现,这使得框架用户可以通过继承DefaultSqlInjector并覆盖该方法来自定义需要注入的通用方法。例如,若某项目不希望注入Delete方法,只需创建一个自定义 Injector 去掉该方法对应的AbstractMethod即可。DefaultSqlInjector提供了 MyBatis-Plus 标准的 CRUD 方法集合,包括Insert、Delete、DeleteByMap、DeleteById、DeleteBatchByIds、Update、UpdateById、SelectById、SelectBatchByIds、SelectByMap、SelectCount、SelectList、SelectPage等。 -
MybatisPlusInterceptor与原生Interceptor的异同?一句话回答:两者都实现
org.apache.ibatis.plugin.Interceptor接口,但MybatisPlusInterceptor内部维护了一个List<InnerInterceptor>链,通过组合方式实现多拦截器的统一管理和顺序控制,比原生多插件通过@Intercepts注解排序更灵活。详细解释:原生 MyBatis 支持配置多个
Interceptor,通过InterceptorChain按顺序执行。然而开发者很难精准控制这些插件的执行先后,尤其是在引入第三方插件时。MybatisPlusInterceptor将拦截逻辑进一步拆分为“主拦截器 + 内部拦截器”两层:主拦截器实现Interceptor接口,负责接收Invocation并判断目标类型(Executor、StatementHandler 等),然后按顺序调用内部拦截器的对应生命周期方法(beforeQuery、beforeUpdate、afterQuery等)。每个InnerInterceptor只关注特定的增强逻辑(分页、乐观锁、多租户等),开发者只需按addInnerInterceptor的添加顺序决定它们的先后执行顺序,彻底解决了排序难题。同时这种设计使得功能模块高度解耦,便于按需组合。 -
分页插件如何执行 count 查询?如何优化?
一句话回答:分页插件在拦截
Executor.query时,通过JSqlParser将原 SQL 解析并改写为 count SQL 单独执行,优化手段包括去除 ORDER BY、不必要的列以及将SELECT *替换为COUNT(*),复杂场景可关闭自动优化并手动编写 count SQL。详细解释:当执行带有
IPage参数的方法时,PaginationInnerInterceptor.beforeQuery首先检查page.searchCount是否为true(默认开启)。若开启,则取出BoundSql中的原始 SQL,交由方言的buildCountSql方法生成 count 语句。这一步利用JSqlParser(若引入)进行优化:移除ORDER BY子句、将非必要的GROUP BY转换为简化形式、将 SELECT 列列表替换为COUNT(*)等。optimizeCountSql开关控制是否启用 JSqlParser 优化(3.4.0+ 默认开启)。优化后生成新的BoundSql并通过Executor.query执行,获取总记录数写入page.setTotal。但JSqlParser对极为复杂的 SQL(如多层嵌套、窗口函数)可能优化不彻底甚至改变语义,此时可设置page.setSearchCount(false)并在业务层手动执行一个专门的轻量 count,或自定义方言实现。 -
逻辑删除与物理删除的实现异同?
一句话回答:逻辑删除不真正删除数据,通过
@TableLogic标记字段,在 SQL 注入阶段将 DELETE 语句转换为UPDATE SET deleted=1,查询自动追加WHERE deleted=0;物理删除则是直接 DELETE 记录。详细解释:MyBatis-Plus 的逻辑删除是在注入阶段完成的。
TableInfoHelper解析实体类时,若发现@TableLogic注解,会记录其未删除值(如 0)和删除值(如 1)。DefaultSqlInjector中的DeleteById、Delete等AbstractMethod在构造SqlSource时,会检查TableInfo是否配置了逻辑删除。若是,则生成UPDATE table SET deleted = 1 WHERE id = ? AND deleted = 0,而非DELETE FROM table WHERE id = ?。同样,所有Select和Update方法的 SQL 模板都会在 WHERE 中自动拼接AND deleted = 0。这意味着逻辑删除的 SQL 在注册时就已经是静态确定的,没有拦截器运行时开销。物理删除则完全不走逻辑删除路径,MyBatis-Plus 仍保留了物理删除方法(如deletePhysicalById)供特殊场景使用。 -
乐观锁插件在并发场景下如何保证更新安全?
一句话回答:乐观锁插件在
Executor.update执行前拦截,自动在 UPDATE 语句的 SET 部分追加version = version + 1,在 WHERE 部分追加version = 查询时的旧版本值,若更新影响行数为 0,则说明版本冲突,重试或抛异常。详细解释:
OptimisticLockerInnerInterceptor实现在beforeUpdate方法中。它先识别传入参数中是否包含带有@Version注解字段的实体对象,通过TableInfoHelper获取当前版本字段名和旧版本值。然后它通过JSqlParser或字符串处理,在原 SQL 的 SET 子句后添加version = version + 1,在 WHERE 条件后添加AND version = #{oldVersion}。注意每次更新时,旧版本值是从实体对象中取出作为新的参数追加到 SQL 参数列表中。执行更新后,MyBatis 返回影响行数。若行数为 0,MyBatis-Plus 会进行冲突检测(取决于配置,默认不自动抛异常,但提供了ParameterUtils等工具辅助判断)。开发者通常结合UpdateWrapper手动设置版本条件,或使用updateById自动体验乐观锁。 -
TableInfoHelper的线程安全与缓存策略?一句话回答:
TableInfoHelper内部使用ConcurrentHashMap作为缓存容器,并在initTableInfo方法上加synchronized锁,确保每个实体类在多线程环境下仅被解析一次,后续读取无锁高性能。详细解释:在 Spring Boot 环境下,
SqlSessionFactory构建可能并发处理多个 Mapper 接口。TableInfoHelper.initTableInfo是静态方法,为防止同一实体类被重复解析,它在方法级使用了synchronized关键字。方法内部首先根据builderAssistant.getCurrentNamespace() + ":" + className生成缓存 key 从 Map 中查询,若未命中则反射读取注解、构建TableInfo对象并放入 Map。解析过程涉及反射和字符串操作,相对较重,一旦完成缓存,后续所有对该实体的访问都直接从ConcurrentHashMap读取,无锁竞争。这种单例 + 懒加载的缓存策略保证了安全与性能。 -
如何自定义通用方法?例如批量插入。
一句话回答:创建一个继承
AbstractMethod的子类,在injectMappedStatement中构造对应的 SQL 和MappedStatement,然后继承DefaultSqlInjector并重写getMethodList,将该自定义方法加入列表即可。详细解释:MyBatis-Plus 提供了丰富的扩展点来自定义通用方法。以批量插入
insertBatch为例:首先创建InsertBatch类,继承com.baomidou.mybatisplus.core.injector.AbstractMethod,实现injectMappedStatement。在其中编写 SQL 模板,例如遍历collection参数拼接多个 VALUES,使用languageDriver.createSqlSource生成SqlSource,然后调用this.addInsertMappedStatement注册。接着创建MySqlInjector继承DefaultSqlInjector,覆盖getMethodList,在 super 返回的列表中添加new InsertBatch()。最后在配置类中将自定义 Injector 设置给MybatisSqlSessionFactoryBean或通过 Spring Boot 的@Bean声明为GlobalConfig的一部分。 -
MyBatis-Plus 的多租户是如何实现的?
一句话回答:通过
TenantLineInnerInterceptor拦截查询和修改操作,动态在 SQL 的 WHERE 条件中追加租户 ID 过滤条件(如AND tenant_id = ?),实现透明化的多租户数据隔离。详细解释:
TenantLineInnerInterceptor同样作为MybatisPlusInterceptor的内部拦截器使用。它通过TenantLineHandler接口供开发者定义如何获取当前租户 ID、哪些表需要过滤、是否忽略特定 SQL 等。在beforeQuery和beforeUpdate回调中,它利用JSqlParser解析原始 SQL 的 WHERE 子句,动态添加tenant_id = currentValue。例如,查询SELECT * FROM order会被改写为SELECT * FROM order WHERE tenant_id = 1。与逻辑删除的静态条件不同,多租户的条件值需在运行时从上下文获取(如 ThreadLocal 中的登录用户租户 ID),因此它采用拦截器运行时改写的方式,而不是 SQL 注入时静态写入。 -
自动注入的
MappedStatement和 XML 中手写的如何共存?一句话回答:两者通过唯一的
id(格式为mapperFullClassName.methodName)区分,存储在Configuration.mappedStatements这个Map<String, MappedStatement>中,互不冲突,执行时根据接口方法全限定名路由到对应的MappedStatement。详细解释:MyBatis 的方法执行核心是
MapperMethod.execute,它会根据接口的method对象拼接出 statement id:mapperInterface.getName() + "." + methodName,然后通过configuration.getMappedStatement(id)获取要执行的 SQL。自动注入的 CRUD 方法(如insert、updateById)的 id 遵循同样的命名规则,只要不与手动编写的 statement id 重名就不会冲突。由于手动 SQL 通常由开发者自定义方法名(如selectUserByOrder),而注入的方法是基类方法名(如selectById),因此它们在 Map 中是天然隔离的。如果开发者需要覆盖某一个注入方法,可以在自己的 XML 或注解中定义一个同名同参数的方法,手动编写的MappedStatement会覆盖注入的(取决于加载顺序,通常手动定义在后,会替换)。 -
MyBatis-Plus 与 JPA 在注解映射上的设计差异?
一句话回答:MyBatis-Plus 的注解(
@TableName、@TableId、@TableField)侧重于 SQL 生成和结果映射,提供细粒度的字段控制(如exist、select、fill);JPA 的注解(@Entity、@Id、@Column)建立在完整的持久化上下文和生命周期管理之上,依赖实体管理器自动脏检查。详细解释:JPA 是一个全自动的 ORM 框架,
@Entity注解标记的类被纳入持久化上下文管理,对象的每一个状态变更最终都会自动同步到数据库(脏检查)。因此 JPA 注解除了列映射,还涉及关系映射(@OneToMany、@ManyToMany)和缓存(一级缓存)等。MyBatis-Plus 本质仍是 SQL 映射框架,其注解主要解决两个问题:表-实体映射(@TableName、@TableId)和字段行为配置(@TableField的fill自动填充、exist是否数据库存在该列、select是否默认查询等)。MyBatis-Plus 没有一级缓存的脏检查机制,SQL 执行时机完全由开发者显式调用 Mapper 方法决定,因此更轻量且对 SQL 掌控度更高。 -
系统设计题:如何在现有 MyBatis-Plus 基础上实现数据审计功能(自动填充创建人、修改人)?
一句话回答:实现
MetaObjectHandler接口,在insertFill和updateFill中通过strictInsertFill/strictUpdateFill方法为标记了@TableField(fill = FieldFill.INSERT)的字段自动设置当前用户和当前时间,利用 MyBatis 的参数处理扩展点,在填充阶段注入审计信息。详细解释:MyBatis-Plus 的自动填充功能是
MetaObjectHandler的实现,它在PreparedStatementHandler.parameterize方法执行前被调用。具体步骤为:创建MyMetaObjectHandler类实现MetaObjectHandler接口,重写insertFill和updateFill。在insertFill中,通过this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now())设置创建时间,通过this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUser())设置创建人。在updateFill中类似设置修改时间和修改人。实体类上相应字段标注@TableField(fill = FieldFill.INSERT)或FieldFill.UPDATE(或FieldFill.INSERT_UPDATE)。获取当前登录用户通常从RequestContextHolder或SecurityContextHolder提取,需要确保填充时 ThreadLocal 中的用户信息已设置。该机制本质上是利用 MyBatis 在执行 SQL 前设置实体的属性值,然后将完整参数交给 ParameterHandler 处理,不侵入业务 SQL 编写。
文末速查表
| 核心组件 | 作用 | 利用的扩展点 |
|---|---|---|
MybatisSqlSessionFactoryBean | 增强 SqlSessionFactory 构建 | SqlSessionFactoryBean |
MybatisConfiguration | 扩展 Configuration | Configuration |
AbstractSqlInjector | 动态注入通用 MappedStatement | MapperBuilderAssistant、MappedStatement 注册 |
MybatisPlusInterceptor | 统一多拦截器链 | Interceptor 接口 |
PaginationInnerInterceptor | 分页 SQL 改写与 count 查询 | Executor 拦截点、BoundSql |
OptimisticLockerInnerInterceptor | 乐观锁版本号自动追加 | Executor 拦截点 |
TableInfoHelper | 实体注解解析与元数据缓存 | 反射、@TableName/@TableId/@TableField |
DefaultSqlInjector | 提供标准 CRUD 方法注入列表 | AbstractSqlInjector 模板方法 |
| 逻辑删除 | DELETE 转 UPDATE、查询追加条件 | @TableLogic、SqlInjector SQL 构造 |
Demo 代码附录
(提供完整可运行项目结构,此处给出核心文件)
pom.xml 关键依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
实体类:
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private Integer age;
@Version
private Integer version;
@TableLogic(value = "0", delval = "1")
private Integer deleted;
// ... getter/setter
}
配置类:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
测试类:
@SpringBootTest
class UserMapperTest {
@Autowired UserMapper userMapper;
@Test
void testCrudAndPageAndLock() {
// 插入
User user = new User(); user.setUsername("demo"); user.setAge(20);
userMapper.insert(user);
// 分页
Page<User> page = new Page<>(1, 10);
userMapper.selectPage(page, Wrappers.lambdaQuery());
System.out.println(page.getRecords().size());
// 乐观锁更新
user.setUsername("updated");
userMapper.updateById(user); // version 自动 +1
// 逻辑删除
userMapper.deleteById(user.getId());
// 查询不再返回被逻辑删除记录
Assertions.assertNull(userMapper.selectById(user.getId()));
}
}
延伸阅读
- MyBatis-Plus 官方文档:baomidou.com
- MyBatis-Plus 源码仓库:github.com/baomidou/my…
- MyBatis 系列之插件开发与拦截链实战(本系列第 7 篇)
- MyBatis 系列之 Spring Boot 整合核心(本系列第 5 篇)
- 深入理解 MyBatis 缓存与拦截器原理 - 《MyBatis 3 源码深度解析》
本文以 MyBatis 扩展点为知识锚点,完整剖析了 MyBatis-Plus 在不修改一行 MyBatis 源码的前提下,如何通过替换核心工厂、扩展拦截器体系和动态注入 MappedStatement 实现通用 CRUD、分页、乐观锁、逻辑删除等高级特性。理解这些增强机制,不仅能够熟练运用 MyBatis-Plus,更能深刻掌握 MyBatis 的可扩展设计哲学,为构建企业内部增强框架提供扎实的参考范式。