链路追踪视角: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);

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

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

1. 获取 Mapper 代理对象

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

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) 时,代理对象 MapperProxy 的 invoke 方法被触发:

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 中定义的所有方法(如 selectById、insert 等)的实现类,例如 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。
  • 字段映射 → 自动推断 name、age 等列名。

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

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

  1. 开发者调用 :userMapper.selectById(1L)。
  2. 代理拦截 :MybatisMapperProxy.invoke → MybatisMapperMethod.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 的"完美补完"。

相关推荐
步行cgn5 小时前
Spring 全注解开发详解
java·后端·spring
IT_陈寒7 小时前
SpringBoot启动慢得像蜗牛?原来是这个配置在捣鬼
前端·人工智能·后端
不才不才不不才9 小时前
Spring 源码系列(29): 20 道高频面试题源码级解析合集
java·后端·spring
索隆zoro9 小时前
Army 的可插拔架构:army-jdbc 与方言模块
java·后端
小蒜学长10 小时前
校园社团招新网站的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·校园社团招新网站
蜗牛互联网11 小时前
从Barclays扩大Claude部署看受监管软件工程的责任重分配
java·大数据·人工智能·后端·软件工程
周杰伦fans13 小时前
C#对话摘要降低Token消耗
人工智能·后端·c#
做系统的大强13 小时前
脚本说 PASS、OS 读全零 —— 到底是谁在撒谎?
后端·操作系统
code2cat14 小时前
【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
java·后端·开发工具·ai agent·mcp
Harvil_15 小时前
任务跑一半,API 突然报“对话超长“断流:我给 Agent 修的紧急逃生通道
后端