目 录CONTENT

文章目录

Mybatis的语句执行逻辑

FatFish1
2025-03-12 / 0 评论 / 0 点赞 / 56 阅读 / 0 字 / 正在检测是否收录...

在Mybatis+spring配置章节中分析过mybatis的配置加载逻辑:

http://www.chymfatfish.cn/archives/configformybatis#mybatis%E6%89%AB%E5%8C%85%E5%92%8C%E5%8A%A0%E8%BD%BDdao%E6%96%87%E4%BB%B6%E7%9A%84%E6%B5%81%E7%A8%8B

简单来说,就是两个核心配置项:MapperScannerConfigurer、SqlSessionFactoryBean

  • MapperScannerConfigurer的作用是扫dao文件,将所有的接口dao生成BeanDefinition,然后将它们的BeanClass配置成MapperFactoryBean.class,这样,在spring生成bean的时候,调用FactoryBean#getObject方法,就会去调用MapperFactoryBean#getObject方法

  • SqlSessionFactoryBean的作用是维护datasource,并且将xml文件扫成代理类,生成的代理类存在内存map,key是namespace对应的类名,value是代理类工厂

当spring创建接口dao对应的bean时,调用MapperFactoryBean#getObject 实际生成的是代理类bean,因此调用接口方法,实际上走的是代理类的invoke方法

因此分析Mybatis语句执行流程,实际上要从代理类的invoke方法开始分析

MapperProxy

invoke

根据jdbcAPI的框架,可以知道开发一个JDBC,只需要完成几个模块:datasource、connection、statement、resultset

http://www.chymfatfish.cn/archives/jdbc#jdbc-api

mybatis搞数据连接,一定也是这么做的,看invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

核心在cachedInvoker(method).invoke(proxy, method, args, sqlSession);

同时这时候代理类已经持有了datasource并且生成session了,这里有个小疑问:代理类里面持有的sqlSession是哪里来的

这个问题可以从MapperFactoryBean#getObject 往上找

// org.mybatis.spring.mapper.MapperFactoryBean#getObject
public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

先看getMapper:

// org.apache.ibatis.session.SqlSessionManager#getMapper
public <T> T getMapper(Class<T> type) {
  return getConfiguration().getMapper(type, this);
}

取的是SqlSession自己,再看getSqlSession()

public SqlSession getSqlSession() {
  return this.sqlSessionTemplate;
}

继续跟踪sqlSessionTemplate的存入:

// org.mybatis.spring.support.SqlSessionDaoSupport
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
    this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
  }
}

这个set方法没有显式的调用点,怀疑是spring创建bean的时候配置属性(populateBean)调用的

http://www.chymfatfish.cn/archives/spring-beansgetfromfactory#populatebean

SqlSessionDaoSupport是MapperFactoryBean的父类,即创建MapperFactoryBean的实调用了setSqlSessionFactory方法,配置了数据源

还记得MapperFactoryBean的创建位置是在MapperScannerConfigurer扫包的时候:

http://www.chymfatfish.cn/archives/configformybatis#beandefinitionregistrypostprocessor---bean%E5%88%9D%E5%A7%8B%E5%8C%96%E5%89%8D%E7%BD%AE%E5%AE%9A%E4%B9%89%E5%A4%84%E7%90%86%E5%99%A8

调用到了ClassPathMapperScanner#processBeanDefinitions

// org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
  definition.getPropertyValues().add("sqlSessionFactory",
      new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
  explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
  definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
  explicitFactoryUsed = true;
}

这里给BD配置了sqlSessionFactory属性,spring创建bean的时候基于set方法配置属性,就让生成的bean持有了sqlSession,进而后面让代理类也持有了sqlSession

了解了sqlSession,继续跟进invoke方法,看cachedInvoker方法和MapperMethodInvoker#invoke,这里实际上是调用PlainMethodInvoker#invoke点我跳转

cachedInvoker

return MapUtil.computeIfAbsent(methodCache, method, m -> {
  if (!m.isDefault()) {
    return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }
  try {
    if (privateLookupInMethod == null) {
      return new DefaultMethodInvoker(getMethodHandleJava8(method));
    }
    return new DefaultMethodInvoker(getMethodHandleJava9(method));
  } ……
});

首先根据!m.isDefault() 区分走哪个实现类

public boolean isDefault() {
    // Default methods are public non-abstract instance methods
    // declared in an interface.
    return ((getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) ==
            Modifier.PUBLIC) && getDeclaringClass().isInterface();
}

这里看isDefault方法,实际上就是判断为public,非抽象方法,即允许接口DAO有default方法实现,有的话不走mybatis,走默认实现

而如果非default,走PlainMethodInvoker#invoke点我跳转

PlainMethodInvoker

invoke

private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;
  public PlainMethodInvoker(MapperMethod mapperMethod) {
    this.mapperMethod = mapperMethod;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}

这里通过成员变量MapperMethod调用MapperMethod#execute方法

MapperMethod

execute

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 SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  ……
  return result;
}

这里就是根据返回值分流了,实际上利用前面传进来的SqlSession实现不同方法分流

可以看到SELECT场景比较多,以返回多条为例,参考executeForMany

executeForMany

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  // part1. 调用执行
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.selectList(command.getName(), param, rowBounds);
  } else {
    result = sqlSession.selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  // part2. 结果封装
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    }
    return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
  }
  return result;
}

part1,根据是否有写列映射决定调用方法

part2,支持返回集合、数组类型

跟进DefaultSqlSession#selectLis

DefaultSqlSession

selectList

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    dirty |= ms.isDirtySelect();
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch ……
}

传进来的statement是xml中的id,则首先根据id获取statement,参考Configuration#getMappedStatement点我跳转

然后下面由executor执行query方法,可以看到executor有BaseExecutor、CachingExecutor两个实现,即mybatis的缓存能力

顺着BaseExecutor#query往下找

// BaseExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
    CacheKey key, BoundSql boundSql) throws SQLException {
  ……
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  ……

看到queryFromDatabase这个方法:

// BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ……
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

然后看BatchExecutor#doQuery

// BatchExecutor#doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
    ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    flushStatements();
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds,
        resultHandler, boundSql);
    Connection connection = getConnection(ms.getStatementLog());
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

终于看到最基础的jdbc套路了,到这里mybatis的执行逻辑基本拆解清楚

Configuration

getMappedStatement

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
  if (validateIncompleteStatements) {
    buildAllStatements();
  }
  return mappedStatements.get(id);
}

跟进buildAllStatements方法就可以看到把xml语句解析成statement的过程

buildAllStatements

x.parseStatementNode();
……
x.resolve();

这里有两个方法,进去都会调用到LanguageDriver#createSqlSource

例如resolve调用parseStatement:

void parseStatement(Method method) {
  final Class<?> parameterTypeClass = getParameterType(method);
  final LanguageDriver languageDriver = getLanguageDriver(method);
  getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
    final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass,
        languageDriver, method);
  ……

看下LanguageDriver#createSqlSource

public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
  // issue #3
  if (script.startsWith("<script>")) {
    XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
    return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
  }
  // issue #127
  script = PropertyParser.parse(script, configuration.getVariables());
  TextSqlNode textSqlNode = new TextSqlNode(script);
  if (textSqlNode.isDynamic()) {
    return new DynamicSqlSource(configuration, textSqlNode);
  } else {
    return new RawSqlSource(configuration, script, parameterType);
  }
}

这里的SqlSource是用来解析sql语句的,根据sql类型,分成了动态的和非动态的,动态sql是一个重点内容,即处理#{}和${}占位符,参考DynamicSqlSource部分

这里在前面MapperRegistry#addMapper 调用parse解析也会触发一次,所以执行的时候一般都是有缓存了

// org.apache.ibatis.binding.MapperRegistry#addMapper
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();

再看buildSqlSource

// org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#buildSqlSource
private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,
    Method method) {
  if (annotation instanceof Select) {
    return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);
  }
  if (annotation instanceof Update) {
    return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);
  } else if (annotation instanceof Insert) {
    return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);
  } else if (annotation instanceof Delete) {
    return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);
  } else if (annotation instanceof SelectKey) {
    return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);
  }
  return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
}

就是根据不同的xml方法分别执行解析了

DynamicSqlSource

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

SqlSource接口的方法就是getBoundSql方法,有几种实现,DynamicSqlSource就是负责解析动态sql的

getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  rootSqlNode.apply(context);
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  context.getBindings().forEach(boundSql::setAdditionalParameter);
  return boundSql;
}

这里构造了一个SqlSourceBuilder是用来解析sql的,继续跟进SqlSourceBuilder#parse点我跳转

SqlSourceBuilder

parse

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
      additionalParameters);
  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  String sql;
  if (configuration.isShrinkWhitespacesInSql()) {
    sql = parser.parse(removeExtraWhitespaces(originalSql));
  } else {
    sql = parser.parse(originalSql);
  }
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

可以看到,使用GenericTokenParser解析占位符,并最终解析成StaticSqlSource返回去

0

评论区