链路追踪视角:MyBatis-Plus 如何基于 MyBatis 封装 BaseMapper

链路追踪视角:MyBatis-Plus 如何基于 MyBatis 封装 BaseMapper

MyBatis-Plus(简称 MP)作为 MyBatis 的增强框架,通过 BaseMapper 提供了通用的 CRUD 操作,极大地提升了开发效率。为了更透彻地理解其封装机制,本文将采用链路追踪的思维,从开发者调用接口开始,逐步深入到 MyBatis-Plus 的核心实现,分析其如何基于 MyBatis 完成对 BaseMapper 的封装。

一、从调用开始:BaseMapper 的使用场景

假设我们有一个简单的实体类 User 和对应的 Mapper 接口:

java 复制代码
@TableName("t_user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    // getter 和 setter 省略
}

public interface UserMapper extends BaseMapper<User> {
}

开发者只需这样调用:

java 复制代码
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);

表面上看,selectByIdBaseMapper 提供的方法,但其背后是如何实现的呢?让我们沿着调用链路逐步追踪。

二、链路追踪:从接口调用到代理执行

1. 获取 Mapper 代理对象

当调用 sqlSession.getMapper(UserMapper.class) 时,MyBatis 的 SqlSession 会委托给 ConfigurationgetMapper 方法:

java 复制代码
// MyBatis: org.apache.ibatis.session.Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

这里的 mapperRegistry 是 MyBatis 的 MapperRegistry 类,但在 MyBatis-Plus 中被替换为 MybatisMapperRegistry。链路进入 MyBatis-Plus 的自定义实现:

java 复制代码
// MyBatis-Plus: com.baomidou.mybatisplus.core.MybatisMapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    return mapperProxyFactory.newInstance(sqlSession);
}
  • 关键点 :MyBatis-Plus 重写了 MapperRegistry,并在启动时通过扫描将所有继承 BaseMapper 的接口注册到 knownMappers 中。
  • 代理生成newInstance 方法创建了一个 MapperProxy 对象,这是 JDK 动态代理的实现。

2. 方法调用拦截

当调用 userMapper.selectById(1L) 时,代理对象 MapperProxyinvoke 方法被触发:

java 复制代码
// MyBatis-Plus: com.baomidou.mybatisplus.core.override.MybatisMapperProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}
  • 链路分叉cachedMapperMethod 会根据方法签名缓存并返回一个 MybatisMapperMethod 对象。
  • 执行逻辑execute 方法根据方法名和参数决定具体的执行路径。

对于 selectById,链路进入 MybatisMapperMethod.execute

java 复制代码
// MyBatis-Plus: com.baomidou.mybatisplus.core.override.MybatisMapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
    if (SqlCommandType.SELECT == command.getType()) {
        if (method.getReturnType().isAssignableFrom(List.class)) {
            return sqlSession.selectList(command.getName(), args);
        }
        return sqlSession.selectOne(command.getName(), args[0]);
    }
    // 其他类型如 INSERT、UPDATE 等略
}
  • 关键点command.getName() 返回的是一个全局唯一的 SQL ID,例如 com.baomidou.mybatisplus.core.mapper.BaseMapper.selectById
  • 执行 SQL :最终调用 MyBatis 的 selectOne 方法执行查询。

三、SQL 注入:BaseMapper 方法的实现来源

问题来了:selectById 的 SQL 是从哪里来的?答案在于 MyBatis-Plus 的 SQL 注入机制。

1. 启动时的 SQL 注入

MyBatis-Plus 在 Spring 容器初始化时,通过 MapperScannerConfigurer 扫描 Mapper 接口,并调用 ISqlInjector 注入通用 SQL:

java 复制代码
// MyBatis-Plus: com.baomidou.mybatisplus.core.injector.DefaultSqlInjector
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    List<AbstractMethod> methodList = getMethodList(mapperClass);
    for (AbstractMethod method : methodList) {
        method.inject(builderAssistant, mapperClass);
    }
}
  • 方法列表getMethodList 返回 BaseMapper 中定义的所有方法(如 selectByIdinsert 等)的实现类,例如 SelectById
  • 注入过程 :以 SelectById 为例:
java 复制代码
// MyBatis-Plus: com.baomidou.mybatisplus.core.injector.methods.SelectById
public void injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    String sql = String.format("<script>SELECT %s FROM %s WHERE %s = #{id}</script>",
        sqlSelectColumns(), tableInfo.getTableName(), tableInfo.getKeyColumn());
    SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
    addSelectMappedStatement(mapperClass, "selectById", sqlSource, modelClass, tableInfo);
}
  • SQL 生成 :根据 TableInfo(通过反射解析实体类)动态生成 SELECT * FROM t_user WHERE id = #{id}
  • 注册 :将生成的 MappedStatement 注册到 MyBatis 的 Configuration 中。

2. TableInfo 的作用

TableInfo 是 MyBatis-Plus 的核心元数据类,通过 TableInfoHelper 在启动时解析实体类:

  • @TableName("t_user") → 表名 t_user
  • @TableId → 主键字段 id
  • 字段映射 → 自动推断 nameage 等列名。

这些信息为 SQL 注入提供了基础数据。

四、链路总结:从调用到执行的全流程

  1. 开发者调用userMapper.selectById(1L)
  2. 代理拦截MybatisMapperProxy.invokeMybatisMapperMethod.execute
  3. SQL 执行sqlSession.selectOne → MyBatis 执行预注入的 MappedStatement
  4. 结果映射 :MyBatis 将查询结果映射为 User 对象返回。

背后支持:

  • 启动时 :扫描 Mapper → 注入通用 SQL → 注册 MappedStatement
  • 运行时:动态代理 → 方法路由 → SQL 执行。

五、与 MyBatis 的协作与增强

  • 复用 MyBatis :动态代理、SqlSession、结果映射等核心机制完全继承自 MyBatis。
  • 增强点
    1. SQL 自动化 :通过 ISqlInjector 注入通用 SQL。
    2. 元数据管理TableInfo 实现实体与表的自动映射。
    3. 条件构造Wrapper 扩展了动态查询能力。

六、结论

MyBatis-Plus 对 BaseMapper 的封装,是在 MyBatis 动态代理和 SQL 执行框架上的巧妙扩展。通过启动时的 SQL 注入和运行时的代理拦截,它实现了通用 CRUD 的零配置使用。链路追踪显示,这种设计既保留了 MyBatis 的灵活性,又通过自动化大幅提升了开发效率,堪称对 MyBatis 的"完美补完"。

相关推荐
追逐时光者5 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_5 小时前
敏捷开发流程-精简版
前端·后端
苏打水com6 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧7 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧7 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧7 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧7 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧7 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng8 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6018 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring