我们知道,mybatisplus的BaseMapper接口中提供了一些如updateById的方法,框架本身已经实现了这些CRUD功能,基本的CRUD我们就没必要写sql,直接使用java语法就能对数据进行操控,很方便。那么这些功能是如何被实现的呢?这是我研读源码的动机
在这里插入图片描述

关键类

  1. AbstractSqlInjector及其子类DefaultSqlInjector
  2. AbstractMethod及其子类
  3. SqlMethod
  4. mapper代理:MybatisMapperProxy
  5. Configuration

MybatisMapperProxy 以代理类作为切入点

BaseMapper是一个接口,里面的方法都是抽象方法,所以很明显框架是通过代理的方式去实现这些抽象方法。

public class MybatisMapperProxy<T> implements InvocationHandler, Serializable {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (method.isDefault()) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }
    ......省略
}

这段代码是来自 MyBatis 库中的 MapperProxy 类,用于为 Mapper 接口创建动态代理实例,通过该实例调用实际的 SQL 语句。

invoke 方法会在动态代理实例上调用,当 Mapper 接口中的方法被调用时,该方法将被执行。其中,proxy 参数是动态代理实例本身,method 参数是被调用的方法,args 参数包含传递给方法的参数。

第一个 if 语句检查方法是否属于 Object 类。如果是,则在 MapperProxy 实例本身上调用该方法,并将 args 数组作为参数传递。

第二个 else if 语句检查方法是否为默认方法,这是在 Java 8 中引入的。如果是默认方法,则使用 invokeDefaultMethod 方法调用它,传递 proxy、method 和 args 参数。

如果方法既不属于 Object 类也不是默认方法,则使用 method 参数从缓存中获取一个 MybatisMapperMethod 实例,并在其上调用 execute 方法,传递 sqlSession 和 args 参数。execute 方法负责使用 SqlSession 对象执行实际的 SQL 语句,并返回结果。

可以看到,BaseMapper的抽象方法的实现是在这两步:
final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);


MybatisMapperMethod#execute主要是一下逻辑
  1. 判断command类型,可以看到类型包括INSERT、UPDATE等;
  2. 转化参数;
  3. command.getName()作为入参执行sqlSession对应方法;
public class MybatisMapperMethod {
    private final MapperMethod.SqlCommand command;
    private final MapperMethod.MethodSignature method;

    public MybatisMapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
        this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
......
    }
}

接下来,要看一下command的type和name具体是什么?

可以看到,将mapper的接口类和方法作为入参,调用resolveMappedStatement方法得到一个
MappedStatement 对象,取出id作为command的name,sqlCommandType作为command的type;

在resolveMappedStatement中,String statementId = mapperInterface.getName() + "." + methodName;将mapper的完全限定名和调用方法名拼接起来,作为statementId;

configuration.getMappedStatement(statementId);然后根据statementId从configuration对象中获取到对应的MappedStatement对象;

另外,这里引申出了一个关键的类com.baomidou.mybatisplus.core.MybatisConfiguration,很显然MybatisConfiguration对象维护了MappedStatement对象集合,所以要想弄明白对应sqlCommandType,需要研究MappedStatement对象如何被加载到MybatisConfiguration中;

  public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }
# 类:org.apache.ibatis.binding.MapperMethod
    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }

以insert为例,继续往下看sqlSession.insert做了哪些操作?

  1. 根据statementId,从configuration对象中获取到对应的MappedStatement,然后调用执行器执行MappedStatement对象;
  2. MybatisSimpleExecutor#doUpdate 中调用configuration对象的newStatementHandler方法创建一个StatementHandler 对象;
  3. prepareStatement(handler, ms.getStatementLog(), false) 生成PreparedStatement对象;
  4. ps.execute()执行sql;
# org.apache.ibatis.session.defaults.DefaultSqlSession

  public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }
 
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
public class MybatisSimpleExecutor extends AbstractBaseExecutor {

    public MybatisSimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }


    @Override
    public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
            stmt = prepareStatement(handler, ms.getStatementLog(), false);
            return stmt == null ? 0 : handler.update(stmt);
        } finally {
            closeStatement(stmt);
        }
    }
    
    private Statement prepareStatement(StatementHandler handler, Log statementLog, boolean isCursor) throws SQLException {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        //游标不支持返回null.
        if (stmt == null && !isCursor) {
            return null;
        } else {
            handler.parameterize(stmt);
            return stmt;
        }
    }
    
......

public class PreparedStatementHandler extends BaseStatementHandler {

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }
......


PreparedStatement对象的创建

  1. boundSql.getSql()获取sql模版;
  2. connection.prepareStatement创建PreparedStatement对象;
  3. BaseStatementHandler#BaseStatementHandler中,获取BoundSql对象;
# org.apache.ibatis.executor.statement.PreparedStatementHandler

  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }
# org.apache.ibatis.executor.statement.BaseStatementHandler

  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

注入抽象方法

事实:
mybatis的mapper类不能有重载方法Mapper类中存在名称相同的方法重载报错,所以生成的MappedStatement的id只由完全类限定名 + 方法名构成。

  1. com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
  2. com.baomidou.mybatisplus.core.injector.AbstractMethod#injectMappedStatement

AbstractSqlInjector#inspectInject 往Mapper类中自动注入自定义方法

# com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject

    @Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        Class<?> modelClass = extractModelClass(mapperClass);
        if (modelClass != null) {
            String className = mapperClass.toString();
            Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
                List<AbstractMethod> methodList = this.getMethodList(mapperClass);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    // 循环注入自定义方法
                    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                } else {
                    logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                }
                mapperRegistryCache.add(className);
            }
        }
    }

自定义方法列表

public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

以deleteById为例

  1. SqlMethod保存了一些Sql模版,如DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "<script>\nDELETE FROM %s WHERE %s=#{%s}\n</script>")

  2. sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty());TableInfo对象中取出表名称、主键列名名称以及主键属性名填充Sql模版;

  3. 如何理解SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class)

    这段代码是 MyBatis 中用于创建 SqlSource 对象的代码。SqlSource 对象是用于封装 SQL 语句的对象,其中包含了 SQL 语句和参数映射信息等元数据。

    在 MyBatis 中,可以使用不同的语言驱动(Language Driver)来支持不同的 SQL 语言,例如 XML、注解或者纯文本等。languageDriver.createSqlSource() 方法就是用于创建相应语言的 SqlSource 对象的。

    具体来说,languageDriver 是一个实现了 LanguageDriver 接口的类的实例,它负责解析 SQL 语句并生成对应的 SqlSource 对象。createSqlSource() 方法接受三个参数:

    • configurationConfiguration 对象,表示 MyBatis 的全局配置信息。
    • sql:表示 SQL 语句的字符串。
    • parameterType:表示 SQL 语句的参数类型,在 SQL 语句中使用 #{} 占位符表示,可以是任意 Java 类型。

    执行该方法后,会根据给定的 SQL 语句和参数类型,使用对应的语言驱动解析 SQL 语句并生成 SqlSource 对象,最后返回该对象。该对象可以被用于执行 SQL 语句,例如在 MappedStatement 中使用。

    在 MyBatis 中,语言驱动(Language Driver)是一个接口,用于支持不同的 SQL 语言,例如 XML、注解或纯文本等。每种 SQL 语言都需要一个对应的语言驱动来解析和处理,从而生成对应的 SqlSource 对象。

    LanguageDriver 接口中定义了两个方法:

    • SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType):用于创建 SqlSource 对象。
    • ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql):用于创建参数处理器,用于处理 SQL 语句中的参数。

    在 MyBatis 中,有三种默认的语言驱动:

    • XMLLanguageDriver:用于解析 XML 格式的 SQL 语句。
    • AnnotationLanguageDriver:用于解析注解格式的 SQL 语句。
    • RawLanguageDriver:用于解析纯文本格式的 SQL 语句。

    可以通过在 Mapper 接口上添加 @Lang 注解来指定使用的语言驱动,例如:

    @Lang(XMLLanguageDriver.class) // 指定使用 XML 格式的 SQL 语句 public interface UserMapper {
        // ... }   
    

    在自定义语言驱动时,需要实现 LanguageDriver 接口并实现其中的方法,从而实现对新格式的 SQL 语句的支持。通常需要重写 createSqlSource() 方法和 createParameterHandler() 方法,分别用于解析 SQL 语句和处理参数。自定义的语言驱动可以通过在 MyBatis 的配置文件中配置来启用。

public class DeleteById extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql;
        SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
        if (tableInfo.isLogicDelete()) {
            sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
                tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
                tableInfo.getLogicDeleteSql(true, false));
            SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
            return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);
        } else {
            sqlMethod = SqlMethod.DELETE_BY_ID;
            sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
                tableInfo.getKeyProperty());
            SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
            return this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);
        }
    }
}

XMLLanguageDriver 是 MyBatis 中默认的语言驱动,用于解析 XML 格式的 SQL 语句。

在 MyBatis 中,可以通过在 Mapper 接口对应的 XML 文件中编写 SQL 语句,从而实现数据访问操作。例如,在 UserMapper.xml 文件中,可以定义一个名为 deleteById 的 SQL 语句:

<script>
	DELETE FROM %s WHERE %s=#{%s}
</script>

在上述 XML 文件中, 元素表示要执行一个删除操作,id 属性指定了该操作的 ID,即 deleteById,#{id} 表示 SQL 语句中的参数。

当使用 XMLLanguageDriver 作为语言驱动时,MyBatis 会解析 XML 文件中的 SQL 语句,并将其转换为对应的 SqlSource 对象,从而实现数据访问操作。

需要注意的是,虽然 XMLLanguageDriver 是 MyBatis 的默认语言驱动之一,但并不是所有的 SQL 方言都可以使用 XML 格式的 SQL 语句来表示。如果需要使用非默认的语言驱动来支持某种 SQL 方言,可以自定义语言驱动并在 MyBatis 的配置文件中进行配置。

  1. this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);这段代码调用了 MapperBuilderAssistant 的 addDeleteMappedStatement() 方法,用于向 MyBatis 的配置对象中添加一个映射语句(MappedStatement)。

    MapperBuilderAssistant 是 MyBatis 中用于辅助构建 Mapper 映射器(Mapper)的类,是向 MyBatis 中添加 Mapper 映射器的关键类之一 。addMappedStatement() 是 MapperBuilderAssistant 类中的一个方法,用于向 MyBatis 的配置对象中添加一个映射语句(MappedStatement)。
    该方法的参数比较多,主要包括:

    1. id:映射语句的唯一标识符,格式为 namespace.<select|insert|update|delete>Id,其中 是 Mapper 接口的命名空间,<select|insert|update|delete>Id 是映射语句的名称,例如 com.example.dao.UserMapper.selectUserById。
    2. sqlSource:封装了 SQL 语句和参数类型的 SqlSource 对象。
    3. statementType:SQL 语句的执行类型(STATEMENT 或 PREPARED)。
    4. sqlCommandType:SQL 语句的类型(SELECT、INSERT、UPDATE 或 DELETE)。
    5. fetchSize:结果集的大小。
    6. timeout:SQL 语句的超时时间。
    7. parameterMap:参数映射器的唯一标识符。
    8. parameterType:参数类型的 Class 对象。
    9. resultMap:结果映射器的唯一标识符。
    10. resultType:结果类型的 Class 对象。
    11. resultSetType:结果集的类型(FORWARD_ONLY、SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE)。
    12. flushCache:是否清空缓存。
    13. useCache:是否使用缓存。
    14. resultOrdered:是否按照结果集顺序返回结果。
    15. keyGenerator:主键生成器。
    16. keyProperty:主键属性。
    17. keyColumn:主键列名。
    18. databaseId:数据库标识符。
    19. lang:语言驱动器。
    20. resultSets:多结果集配置。

    通过调用 addMappedStatement() 方法,我们可以向 MyBatis 的配置对象中添加一个映射语句,从而使得 MyBatis 能够根据该映射语句的配置信息执行相应的 SQL 语句,并将执行结果映射成 Java 对象。

    com.baomidou.mybatisplus.core.injector.AbstractMethod#addMappedStatement
    
       protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
                                                 SqlCommandType sqlCommandType, Class<?> parameterType,
                                                 String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
                                                 String keyProperty, String keyColumn) {
        String statementName = mapperClass.getName() + DOT + id;
        if (hasMappedStatement(statementName)) {
            logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
            return null;
        }
        /* 缓存逻辑处理 */
        boolean isSelect = false;
        if (sqlCommandType == SqlCommandType.SELECT) {
            isSelect = true;
        }
        return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType,
            null, null, null, parameterType, resultMap, resultType,
            null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
            configuration.getDatabaseId(), languageDriver, null);
    }
    

更多推荐