MyBatis的解析和运行原理(源码分析)

本文将结合源码,分析mybatis基本的运行原理。导入Idea:参考

MyBatis的解析和运行原理一览图

MyBatis的运行过程分为两大步:

  • 读取配置文件缓存到Configuration对象,用以创建SqlSessionFactory;
  • SqlSession的执行过程。

MyBatis底层架构的基础掌握:

  • 反射技术
  • 动态代理技术

1. 构建SqlSessionFactory的过程

构建的两大步:

  • 第一步:通过org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML文件,对出所配置的参数,并将读取的能容存入org.apache.ibatis.session.Configuration类对象中。而Configuration采用的是单例模式,几乎所有的MyBatis配置内容都会存放在这个单例对象中,以便后续将这些内容读出。
  • 第二步:使用Configuration对象去创建SqlSessionFactory。MyBatis中的SqlSessionFactory是个接口,而不是一个实现类,为此MyBatis提供了一个默认的实现类org.apache.ibatis.session.default.DefaultSqlSessionFactory。在大部分情况下都没有必要自己创建新的SqlSessionFactory实现类。

这种创建的方式就是一种Builder模式,对于复杂的对象而言,使用构造参数很难实现。这时使用一个类Configuration作为统领,一步步构建所需要的内容,最终获取对象。

1.1 构建Configuation

首先来看看,xml是如何解析的。用XMLConfigBuider解析XML的源码。我们来看XMLConfiguration中的一段源码:

package org.apache.ibatis.builder.xml;

/**inmports  略 **/
public class XMLConfigBuilder extends BaseBuilder {
    。。。。。。
        
     private void parseConfiguration(XNode root) {
        try {
              //issue #117 read properties first
              propertiesElement(root.evalNode("properties"));
              Properties settings = settingsAsProperties(root.evalNode("settings"));
              loadCustomVfs(settings);
              loadCustomLogImpl(settings);
              typeAliasesElement(root.evalNode("typeAliases"));
              pluginElement(root.evalNode("plugins"));
              objectFactoryElement(root.evalNode("objectFactory"));
              objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
              reflectorFactoryElement(root.evalNode("reflectorFactory"));
              settingsElement(settings);
              // read it after objectFactory and objectWrapperFactory issue #631
              environmentsElement(root.evalNode("environments"));
              databaseIdProviderElement(root.evalNode("databaseIdProvider"));
              typeHandlerElement(root.evalNode("typeHandlers"));
              mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          	throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
}

从源码中我们可以看到,它是根据xml的标签节点名称一步步解析内容并得到对应的信息,而这些信息正是我们所配置的内容。那么这些读取的信息都去了哪里呢? 如typeHandlers的内容,读取后就注册到了TypeHandlerRegistry对象当中。XMLConfigBuilder类还继承了BaseBuilder类。我来看看BaseBuilder的部分源码:

package org.apache.ibatis.builder;

import ......

public abstract class BaseBuilder {
  protected final Configuration configuration;
  protected final TypeAliasRegistry typeAliasRegistry;
  protected final TypeHandlerRegistry typeHandlerRegistry;

  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
......

看到了,BaseBuilder定义了单例的configuration,而typeHandlerRegistry又是configuration的一个单例。不难想到,我们配置的typeHandlers内容被读取到先注册到了typeHandlerRegistry对象里面,然后最后又到了Configuration对象里。其他的配置也是相同的道理,最后能通过Configuration对象来获取。

1.2 Configuration的作用

在SqlSessionFactory构建中,Configuration是最重要的,它包含如下的作用:

  1. 读入配置文件,包括基础的配置文件XML和映射器的XML(或注解)。
  2. 初始化一些基础的配置,比如MyBatis的别名等;一些重要的类对象,如插件、映射器、Object工厂、typeHandlers对象等等。
  3. 提供单例,为后续创建SessionFactory服务,提供配置的参数。
  4. 执行一些重要的初始化对象方法。

关于初始化,它会初始化如下的内容:

  • properties 全局参数
  • typeAliases 别名
  • Plugins 插件
  • objectFactory 对象工厂
  • reflectionFactory 对象包装工厂
  • settings 环境设置
  • environments 数据库环境
  • databaseIdProvider 数据库标识
  • typeHandlers 类型转换器
  • Mappers 映射器

1.3 构建映射器(Mapper)的内部组成

同样的,当XMLMapperBuilder解析XML时,会将每一个SQL和其配置的内容保存下来,那么他是怎么保存的呢?

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

读取完后最后到了这里 MappedStatement:

package org.apache.ibatis.mapping;

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
......
}

着这里我么就能找到我们Mapper里面所配置的东西。其中private SqlSource sqlSource;是提供BoundSql的地方,它是一个接口,而不是一个实现类。表示从XML文件或注释中读取的映射语句的内容。它创建将从用户接收的输入参数传递到数据库的SQL。

package org.apache.ibatis.mapping;

/**
 * Represents the content of a mapped statement read from an XML file or an annotation.
 * It creates the SQL that will be passed to the database out of the input parameter received from the user.
 *
 * @author Clinton Begin
 */
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

BoundSql是一个结果对象:

package org.apache.ibatis.mapping;

public class BoundSql {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }
    ......
}

2. SqlSessiion运行过程

2.1 映射器(Mapper)的动态代理

先来看看MyBatis是如何实现getMapper方法的:

public class DefaultSqlSession implements SqlSession {
    ......
    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }
    ......
}

我们看到它调用了Configuration对象的getMapper方法:

public class Configuration {
    ......
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
    ......
}

它又运用了Mapperregistry来获取对应的接口对象

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

首先,它会判断是否注册了一个Mapper,如果没有则会抛出异常信息。如果有,就会启用MapperProxyFactory工厂来生成一个代理实例

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.session.SqlSession;

/**
 * @author Lasse Voss
 */
public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

Mapper映射是通过动态代理来的

@Override
  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 MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

使用了JDK动态代理,先判断是不是类,使得话直接执行invoke方法。但是这里的Mapper是一个接口,所以生成了一个MapperMethod对象,通过cachedMapperMethod方法对其初始化,然后执行execute方法,把SqlSession和当前运行的参数传递过去。

 public Object execute(SqlSession sqlSession, Object[] args) {
     ......
	result = executeForMap(sqlSession, args);
     ......
 }

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    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
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

MapperMethod类采用命令模式运行,最终通过SqlSession去运行对象的SQL而已。

总结:到了这里我们就能知道MyBatis为什么只用Mapper接口就可以运行了,因为Mapper的XML文件的命名空间对应的是这个接口的全限定名,而方法就是那条SQL的id,这样MyBatis就可以根据全路径和方法名,将其和代理对象绑定起来。通过动态代理技术,让这个接口运行起来,而后采用命令模式。最后使用SqlSession接口的方法使得它能够执行对应的SQL。

2.2 SqlSession的四大对象

  • Executor代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL。其中StatementHandler是最重要的。
  • StatementHandler的作用是使用数据库的Statement(PtreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  • ParameterHandler是用来处理SQL参数的。
  • ResultSetHandler是进行数据集(ResultSet)的封装返回处理的,它相当复杂,好在我们不经常用到它。

Exector-执行器

SqlSession其实只是一个门面,真正干活的是执行器。是一个真正执行Java和数据库交换的对象。有三种执行器(settings元素中的defaultExecutor可配置):

  • SIMPLE -简易执行器,没什么特别的,默认的执行器
  • REUSE-它是一种能够执行重用预处理语句的执行器。
  • BATCH-执行器重用语句和批量更新,批量专用的执行器。

Configuration类中创建执行器:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

判断配置类型确定创建哪种Executor,它的缓存则用CachingExecutor进行包装。最后interceptorChain.pluginAll(executor)运用插件的相关代码。

我们用SimpleExecutor来看看Exector是如何调度的:

public class SimpleExecutor extends BaseExecutor {

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

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
}

configuration.newStatementHandle还是通过Configuration来创建StatementHandler的。然后使用prepareStatement对Sql编译和参数进行初始化。

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

prepareStatement实际进行调用了prepare方法进行预编译与基础设置,然后通过parameterize方法设置参数,最后使用StatementHandler的query方法,把resultHandler传递。

StatementHandler-数据库会话器

同样是Configuration对象生成:

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

new RoutingStatementHandler很明显,创建的真实对象是RoutingStatementHandler。它实现了StatementHandler接口,用代理对象一层层的封装。

然鹅RoutingStatementHandler却不是真实服务的对象。它是通过适配器模式来找到对应的StatementHandler来执行的。与Excutor一样,RoutingStatementHandler分为三种:

  • SimpleStatementHandler 对应JDBC的 Statement
  • PreparedStatementHandler 对应JDBC的 PreparedStatement
  • CallableStatementHandler 对应JDBC的 CallableStatement
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }
}

它定义了一个适配器-------delegate,适配器的作用就不说明了。接下来看看返回的StatementHandler是怎么执行查询的,以最常用的PreparedStatementHandler为例:

package org.apache.ibatis.executor.statement;

import ......;

public class PreparedStatementHandler extends BaseStatementHandler {

  public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
  }

  @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;
  }

  @Override
  public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.addBatch();
  }

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

  @Override
  public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleCursorResultSets(ps);
  }

  @Override
  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);
    }
  }

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

}

父类BaseStatementHandler:

@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

如上,instantiateStatement()方法是对SQL进行了预编译,然后做些基础配置,比如超时、获取的最大行数等的设置。Executor会调用parameterize()方法去设置参数。显然,这个时候设置参数的方法是由ParameterHandler来完成的。然后执行参数和SQL都被prepare()方法预编译了,参数在parameterize()方法中已经设置了,所以只要返回结果就可以了。执行后我们就能看到ResultSetHandler返回。

到这里,MyBatis执行SQL的流程就比较清晰了。

再来看看其他细节

ParameterHandler--参数处理器

使用它来对预编译语句进行参数设定的:

package org.apache.ibatis.executor.parameter;

import java.sql.PreparedStatement;
import java.sql.SQLException;

public interface ParameterHandler {
  Object getParameterObject();//返回参数对象
  void setParameters(PreparedStatement ps) throws SQLException; //设置预编译SQL语句的参数
}

具体的实现类:

package org.apache.ibatis.scripting.defaults;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class DefaultParameterHandler implements ParameterHandler {

  private final TypeHandlerRegistry typeHandlerRegistry;

  private final MappedStatement mappedStatement;
  private final Object parameterObject;
  private final BoundSql boundSql;
  private final Configuration configuration;

  public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    this.mappedStatement = mappedStatement;
    this.configuration = mappedStatement.getConfiguration();
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    this.parameterObject = parameterObject;
    this.boundSql = boundSql;
  }

  @Override
  public Object getParameterObject() {
    return parameterObject;
  }

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

}

分析可知,它还是从parameterObject对象中取到参数,然后使用typeHandler转换,如果没设置,根据签名注册的typeHandler对象参数进行处理。而typeHandler也是在MyBatis初始化时,注册在Configuration里面的,需要时就可以直接拿过来用。

MyBatis就是这样完成参数设置的。

ResultSetHandler----结果处理器

package org.apache.ibatis.executor.resultset;

import org.apache.ibatis.cursor.Cursor;

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

SqlSession运行总结

原文地址:https://www.cnblogs.com/nm666/p/11337169.html