MyBatis-Plus原理分析

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 操作(selectByIdinsertupdateById)最终都对应着一个 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 初始化完成之后、在你第一次调用之前,把 selectByIdinsertupdateByIddeleteById 这些方法的 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

MybatisSqlSessionFactoryBeangetObject() 完成初始化后会立即调用一次 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,其实只覆盖了三个点:

覆盖点原生 MyBatisMyBatis Plus
SqlSessionFactorySqlSessionFactoryBeanMybatisSqlSessionFactoryBean
Configurationorg.apache.ibatis.session.ConfigurationMybatisConfiguration(运行时重试注入)
初始化回调ISqlInjectorDefaultSqlInjector → 8 个 AbstractMethod

剩下的 SQL 执行引擎、缓存、插件拦截链——全是 MyBatis 的原始能力。


九、适用场景

BaseMapperselectByIdinsertupdateById 能覆盖 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 的条件构建链路,都可以用同样的"切入口到源码"方式拆解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值