MyBatis是纸老虎吗?(六)

经过前面一些列文章的梳理,我们已将MyBatis框架所需要的资源都准备好了:数据库连接信息储存在Configuration对象中的Environment属性中(该对象中有这样几个属性String类型的id,TransactionFactory类型的transactionFactory、DataSource类型的dataSource。其中DataSource属性中存放了数据库的连接信息,包括要使用的数据库ip、端口、用户名、密码等等;TransactionFactory在此系列文章中的实际类型为JdbcTransactionFactory,这个对象的创建是通过查找别名拿到实际类型然后通过反射方式创建的,这个过程可以参见《MyBatis是纸老虎吗?(二)》这篇文章;id则是开发者自己指定的,这里的值是environment);sql命令信息存储在Configuration对象中Map<String, MappedStatement> 类型的mappedStatements属性中(这个数据初始化过程可翻阅《MyBatis是纸老虎吗?(五)》这篇文章);拦截器信息存储在Configuration对象中InterceptorChain类型的interceptorChain属性中(这个数据初始化过程可翻阅《MyBatis是纸老虎吗?(四)》这篇文章);别名信息存储在Configuration对象中TypeAliasRegistry类型的typeAliasRegistry属性中(这是一个辅助性质的信息,主要是方便框架使用,同时也是为了方便开发者使用,其初始化过程可翻阅《MyBatis是纸老虎吗?(三)》这篇文章)。看着这些准备好的数据,我们要怎么利用它们操作数据库呢?在开始操作之前,我们先来回忆一下jdbc方式操作数据库的具体步骤(这个信息可以从《MyBatis是纸老虎吗?(一)》这篇文章中看到):

  1. 加载并注册 JDBC 驱动程序(Class.forName("com.mysql.cj.jdbc.Driver"))
  2. 建立数据库连接
  3. 创建 SQL 语句对象和结果集对象
  4. 执行sql操作
  5. 关闭连接和释放资源

对照这个步骤,MyBatis中应该也相应的操作步骤,但与手动编写方式不同,MyBatis是通过一种更加标准的方式操作的,这个操作的具体过程是怎样的呢?

1 加载jdbc驱动

首先来看一下MyBatis是如何加载jdbc驱动。我们都知道“加载jdbc驱动”操作是应用程序后面操作mysql数据库的必要条件,那这个过程究竟是怎样的呢?通过前面的学习我们知道在解析MyBatis配置文件的时候,会解析一个名字为environments的节点。这个节点下会有一个dataSrouce节点,这个节点在MyBatis配置文件解析时也会被解析出来。如果没有记错这个节点的默认类型为POOLED,通过MyBatis的别名体系,我们可以确定这个值表示的是PooledDataSourceFactory,而这个工厂类的getDataSource()方法最终返回的是一个类型为PooledDataSource的对象。这个对象是在PooledDataSourceFactory的构造方法中创建的。这个构造方法的源码是这样的:

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }
}

顺着new PooledDataSource()这段代码看下去,在PooledDataSource类的构造方法中找到了这样一句代码:

public PooledDataSource() {
  dataSource = new UnpooledDataSource();
}

就是new UnpooledDataSource()这句代码会触发jvm加载UnpooledDataSource字节码。从这个类中我们会看到这样一段静态码:

static {
  Enumeration<Driver> drivers = DriverManager.getDrivers();
  while (drivers.hasMoreElements()) {
    Driver driver = drivers.nextElement();
    registeredDrivers.put(driver.getClass().getName(), driver);
  }
}

在这段代码中我们看到了一个熟悉的名字Driver,难道jdbc驱动的加载是在这里完成的?答案是是的!很抱歉,看到这里我兴奋得不知道说什么了。那就让我们继续疯狂下去吧!这里主要看DriverManager类的getDrivers()方法,该方法及其关联方法的源码如下所示:

@CallerSensitive
public static Enumeration<Driver> getDrivers() {
    // 下面这个方法是加载mysql数据库驱动的地方
    ensureDriversInitialized();
    return Collections.enumeration(getDrivers(Reflection.getCallerClass()));
}

private static void ensureDriversInitialized() {
    if (driversInitialized) {
        return;
    }

    synchronized (lockForInitDrivers) {
        if (driversInitialized) {
            return;
        }
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty(JDBC_DRIVERS_PROPERTY);
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try {
                    while (driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers != null && !drivers.isEmpty()) {
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }

        driversInitialized = true;
        println("JDBC DriverManager initialized");
    }
}

通过跟踪这段源码,我发现在这段源码中最重要部分是从AccessController.doPrivileged开头到println("DriverManager.initialize: jdbc.drivers = " + drivers)这段(这段代码已经加红)。在这段代码中我们重点关注driversIterator.hasNext()这句。首先driversIterator对象的创建是在ServiceLoader类中的iterator()方法中完成的,该方法的源码如下所示:

public Iterator<S> iterator() {

    // create lookup iterator if needed
    if (lookupIterator1 == null) {
        lookupIterator1 = newLookupIterator();
    }

    return new Iterator<S>() {

        // record reload count
        final int expectedReloadCount = ServiceLoader.this.reloadCount;

        // index into the cached providers list
        int index;

        /**
         * Throws ConcurrentModificationException if the list of cached
         * providers has been cleared by reload.
         */
        private void checkReloadCount() {
            if (ServiceLoader.this.reloadCount != expectedReloadCount)
                throw new ConcurrentModificationException();
        }

        @Override
        public boolean hasNext() {
            checkReloadCount();
            if (index < instantiatedProviders.size())
                return true;
            return lookupIterator1.hasNext();
        }

        @Override
        public S next() {
            checkReloadCount();
            S next;
            if (index < instantiatedProviders.size()) {
                next = instantiatedProviders.get(index);
            } else {
                next = lookupIterator1.next().get();
                instantiatedProviders.add(next);
            }
            index++;
            return next;
        }

    };
}

所以driversIterator.hasNext()这句代码实际上走的是上面这段代码中的hasNext()方法,这个方法位于lookupIterator1对象上,这个对象是通过newLookupIterator()方法创建的,这个方法的源码如下所示:

private Iterator<Provider<S>> newLookupIterator() {
    assert layer == null || loader == null;
    if (layer != null) {
        return new LayerLookupIterator<>();
    } else {
        Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
        Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
        return new Iterator<Provider<S>>() {
            @Override
            public boolean hasNext() {
                return (first.hasNext() || second.hasNext());
            }
            @Override
            public Provider<S> next() {
                if (first.hasNext()) {
                    return first.next();
                } else if (second.hasNext()) {
                    return second.next();
                } else {
                    throw new NoSuchElementException();
                }
            }
        };
    }
}

通过源码可以知道lookupIterator1对象的hasNext()方法最终会调用first和second两个对象的hasNext()方法。其中second是最重要的,其实际类型为LazyClassPathLookupIterator。上面代码中second.hasNext()的实际执行过程为:

  1. LazyClassPathLookupIterator#hasNext()
  2. LazyClassPathLookupIterator#hasNextService()
  3. LazyClassPathLookupIterator#nextProviderClass(),在这段代码中,会生成一个字符串,其值为:META-INF/services/java.sql.Driver。接着代码loader.getResource(fullName)会加载这个路径下的java.sql.Driver文件,并解析出其中的数据。注意:这个文件存在于mysql驱动程序包中。拿到这个数据后,这个方法会执行Class.forName("com.mysql.cj.jdbc.Driver", false, loader)这句代码加载mysql驱动

至此,我们在MyBatis框架中找到了加载数据库驱动的代码。那接下来就可以进行下面的步骤了。在开始之前,我想梳理一下这段梳理中出现的PooledDataSource的继承体系,具体如下图所示(关于PooledDataSourceFactory的继承体系,可以参看《MyBatis是纸老虎吗?(三)》这篇文章):

下面再一起回顾一下在《MyBatis是纸老虎吗?(三)》这篇文章中梳理过Environment的解析过程,具体步骤为:

  1. 执行XMLConfigBuilder#parse()
  2. 执行XMLConfigBuilder#parseConfiguration()
  3. 执行XMLConfigBuilder# environmentsElement ()

2 建立数据库连接

上一小节我们梳理了MyBatis加载数据库驱动的过程,这一小节会重点梳理MyBatis创建数据库连接的过程。在开始前,先说一些题外话:

在代码执行过程中,程序会首先调用DefaultSqlSessionFactory的openSession()方法创建一个SqlSession对象,实际的创建动作实在DefaultSqlSessionFactory类的openSessionFromDataSource()方法中完成的,这个方法的执行逻辑是:从Configuration对象中拿到Environment对象;从Environment对象中拿到TransactionFactory对象(实际类型为JdbcTransactionFactory)调用TransactionFactory接口中的newTransaction()方法,创建Transaction对象(注意该对象中的属性有:Connection、DataSource、TransactionIsloationLevel、boolean类型的autoCommit、boolean类型的skipSetAutoCommitOnClose);最后通过new语法创建一个SqlSession对象,即DefaultSqlSession对象,这个对象持有一个Configuration对象、一个Executor对象和一个boolean类型的autoCommit值,这里需要注意Executor对象持有一个Transaction对象。所以《MyBatis是纸老虎吗?(二)》这篇文章中遗留的“Transaction和SqlSession之间存在着怎样的关系?”这个问题的答案应该是这样的SqlSession通过Executor间接持有了Transaction对象

在这个题外话中,我们知道了SqlSession和Transaction之间的关系,但这与我们的问题之间好像没多大联系。不过也不能这么讲,SqlSession可以理解成MyBatis与mysql之间的一个会话,通过这个对象我们可以与数据库之间进行交流,也就是完成数据库中相关数据的增删查改。那我们问题的答案会不会隐藏在这里面呢?下面就一起看一下下面这句代码的执行流程:

Object obj = session.selectOne("org.com.chinasofti.springtransaction.UserDao.queryCountByCondition");
  1. 调用DefaultSqlSession中的selectOne()方法,该方法接收一个参数:sql标识符
  2. 调用DefaultSqlSession中的同名selectOne()方法,该方法接收两个参数:sql标识符和参数列表
  3. 继续调用DefaultSqlSession中的selectList()方法,该方法接收两个参数:sql标识符和参数列表
  4. 接着继续调用本类同名的selectList()方法,该方法接收三个参数:sql标识符、参数列表和RowBounds
  5. 然后继续调用本类同名的selectList()方法,该方法接收了四个参数:sql标识符、参数、RowBounds、ResultHandler。该方法首先从Configuration对象中拿到sql标识符对应的MappedStatement对象,接着继续调用DefaultSqlSession对象持有的Executor对象(这里的实际类型是CachingExecutor)的query()方法
  6. 调用CachingExecutor类上的query()方法,该方法接收了四个参数:sql标识符、参数、RowBounds、ResultHandler。该方法会首先调用MappedStatement对象上的getBoundSql()方法,该方法会返回一个BoundSql对象。接着会调用本类上的createCacheKey()方法,该方法会返回一个CacheKey对象。最后调本类上同名的query()方法
  7. 调用CachingExecutor类上的query()方法,该方法接收了六个参数:MappedStatement对象、Object类型的参数、RowBounds对象、ResultHandler对象、CacheKey对象及BoundSql对象。关于该方法的详细执行步骤,这里不再赘述,有兴趣的可以参看源码。这里重点看这样一句代码:delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
  8. 调用BaseExecutor类上的query()方法,该方法接收了六个参数:MappedStatement对象、Object类型的参数、RowBounds对象、ResultHandler对象、CacheKey对象及BoundSql对象。注意这里的实际类型是SimpleExecutor。这个方法中重点关注queryFromDataBase()方法
  9. 调用BaseExecutor类上的queryFromDatabase()方法,该方法接收了六个参数:MappedStatement对象、Object类型的参数、RowBounds对象、ResultHandler对象、CacheKey对象及BoundSql对象。注意这里的实际类型是SimpleExecutor。这个方法中的重点是调用doQuery()方法的这行代码
  10. 调用SimpleExecutor类上的doQuery()方法,该方法接收了五个参数:MappedStatement对象、Object参数、RowBounds对象、ResultHandler对象、BoundSql对象。先来看一下doQuery()方法的源码,如下所示
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
    BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 从MappedStatement对象中拿到Configuration对象
    Configuration configuration = ms.getConfiguration();
    // 从Configuration对象中创建一个StatementHandler对象
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
        boundSql);
    // 在开始执行数据库操作前,做一些预处理,比如获取数据库连接、sql命令预处理等
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

// 这里我们在罗列一下prepareStatement()方法的源码
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;
}

// 下面再来看一下getConnection()方法的源码
protected Connection getConnection(Log statementLog) throws SQLException {
  // 下面这行代码的执行流程为:JdbcTransaction#getConnection()->JdbcTransaction# openConnection()->DataSource#getConnection()[实际调用的是PooledDataSource类中的getConnection()方法]->PooledDataSource#popConnection()[注意刚进来时conn对象为空,所以会走到else if分支,然后调用UnpooledDataSource类中的getConnection()方法]->UnpooledDataSource类中的getConnection()-> UnpooledDataSource类中的doGetConnection()方法->UnpooledDataSource类中的doGetConnection()方法
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  }
  return connection;
}

// 下面看一下doGetConnection()方法 
private Connection doGetConnection(Properties properties) throws SQLException {
  // 如果数据库驱动未加载,则执行加载jdbc驱动操作,详情可参看源码
  initializeDriver();
  // 获取数据库连接,这个代码与手动编写jdbc代码的写法是一致的
  Connection connection = DriverManager.getConnection(url, properties);
  // 对获取的数据库连接Connection对象进行一些设置,比如是否自动提交、事务级别、连接超时时间等等
  configureConnection(connection);
  return connection;
}

private synchronized void initializeDriver() throws SQLException {
  if (!registeredDrivers.containsKey(driver)) {
    Class<?> driverType;
    try {
      if (driverClassLoader != null) {
        driverType = Class.forName(driver, true, driverClassLoader);
      } else {
        driverType = Resources.classForName(driver);
      }
      // DriverManager requires the driver to be loaded via the system ClassLoader.
      // https://www.kfu.com/~nsayer/Java/dyn-jdbc.html
      Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
      DriverManager.registerDriver(new DriverProxy(driverInstance));
      registeredDrivers.put(driver, driverInstance);
    } catch (Exception e) {
      throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
    }
  }
}

private void configureConnection(Connection conn) throws SQLException {
  if (defaultNetworkTimeout != null) {
    conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
  }
  if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
    conn.setAutoCommit(autoCommit);
  }
  if (defaultTransactionIsolationLevel != null) {
    conn.setTransactionIsolation(defaultTransactionIsolationLevel);
  }
}

通过上述源码我们找到了MyBatis获取数据库连接的代码,这与我们手动编写jdbc时获取数据库连接的代码毫无区别,详情参见下面这幅图片:

通过上述梳理我们找到了手动编写jdbc的前两步在MyBatis中的实现,这更加深了我们对MyBatis的认识,同时让我们对手动编写jdbc访问数据库的步骤有了更深的理解。那访问数据库其他步骤在MyBatis中是如何实现的呢?

3 创建 SQL 语句对象和结果集对象

从标题不难看出,这一小节将SQL对象和结果集对象放到了一起,但实际上这是两个步骤,并且分别位于数据库命令执行的前后:只有有了SQL对象,才有后面数据库命令的执行,才会有后面的结果集对象。但为了与《MyBatis是纸老虎吗?(一)》这篇文章的第一个案例中的操作步骤保持一致,这里将SQL语句对象和结果集的处理放在一起梳理。在上一小节中,我们梳理到了“调用SimpleExecutor类上的doQuery()方法,该方法接收了五个参数:MappedStatement对象、Object参数、RowBounds对象、ResultHandler对象、BoundSql对象。”这一步,后面通过源码梳理到了SimpleExecutor类的prepareStatement()方法,这个方法的源码这里再展示一下:

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

从源码可以看到这段代码会创建一个Statement对象,是通过调用StatementHandler接口的prepare()方法完成的。注意这里StatementHandler的实际类型是RoutingStatementHandler【该对象会持有实际类型为PrepareStatementHandler的StatementHandler对象】,这里先看一下StatementHandler的继承结构图,这个结构图在《MyBatis是纸老虎吗?(二)》这篇文章中有梳理过,这里再贴一下图片:

通过源码结合图片我们可以知道RoutingStatementHandler是一个做代理分发的类,也就是这个类对象在创建时会根据MappedStatement中的statementType属性的实际类型创建实际的StatementHandler对象,然后赋值给RoutingStatementHandler类中的delegate属性(在本案例中实际类型是PrepareStatementHandler)。接下来继续看PrepareStatementHandler类中的prepare()方法【注意:这个方法位于BaseStatementHandler类中】及其关联方法的源码,如下所示:

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  // boundSql中保存了实际的sql语句,比如案例中的select count(*) from tb_name
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    // 调用子类实现的模板方法instantiateStatement()
    statement = instantiateStatement(connection);
    // 调用setStatementTimeout()方法为Statement命令设置超时时间
    setStatementTimeout(statement, transactionTimeout);
    // 调用setFetchSize()方法为Statement命令设置fetchSize属性
    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);
  }
}

// PrepareStatementHandler类实现的instantiateStatement()方法
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, Statement.RETURN_GENERATED_KEYS);
    } else {
      return connection.prepareStatement(sql, keyColumnNames);
    }
  }
  if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    return connection.prepareStatement(sql);
  } else {
    return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(),
        ResultSet.CONCUR_READ_ONLY);
  }
}

protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException {
  Integer queryTimeout = null;
  if (mappedStatement.getTimeout() != null) {
    queryTimeout = mappedStatement.getTimeout();
  } else if (configuration.getDefaultStatementTimeout() != null) {
    queryTimeout = configuration.getDefaultStatementTimeout();
  }
  if (queryTimeout != null) {
    stmt.setQueryTimeout(queryTimeout);
  }
  StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout);
}

protected void setFetchSize(Statement stmt) throws SQLException {
  Integer fetchSize = mappedStatement.getFetchSize();
  if (fetchSize != null) {
    stmt.setFetchSize(fetchSize);
    return;
  }
  Integer defaultFetchSize = configuration.getDefaultFetchSize();
  if (defaultFetchSize != null) {
    stmt.setFetchSize(defaultFetchSize);
  }
}

上面源码中主要看instantiateStatement()方法,这个方法定义在BaseStatementHandler中,是一个抽象方法,而上面源码中展示的实现逻辑位于PrepareStatementHandler类中,这个方法处理逻辑为:

  1. 拿到sql命令字符串,比如本例中的select count(*) from tb_user
  2. 调用MappedStatement对象中的getKeyGenerator()方法,看其是否是Jdbc3KeyGenerator类型,如果是则执行相关逻辑(为sql语句中的主键执行数据初始化),否则继续后面代码
  3. 调用MappedStatement对象中的getResultSetType ()方法,看起是否是ResultSetType.DEFAULT。如果是则执行if分支,即connection.prepareStatement(sql);如果不是则执行else分支

看到不知道大家是否想起了《MyBatis是纸老虎吗?(一)》这篇文章中的案例一,其中有这样一句被注释的代码:PreparedStatement preparedStatement = connection.prepareStatement("select * from table_name where id = ?")。是不是一模一样?看到这里我有点迫不及待地想看到执行sql语句的地方。但很抱歉,这里创建的PreparedStatement类型地命令,我们还需要对其中的占位符进行处理,那这个处理逻辑在哪里呢?

回到本小节开始时展示的那段代码中,在调用StatementHandler接口中的prepare()方法创建Statement对象后,会继续调用StatementHandler类的parameterize()方法,与前面梳理的prepare()方法类似,这段代码会先进入RoutingStatementHandler类的parameterize()方法,然后再调用PreparedStatementHandler类中的parameterize()方法,该方法的源码如下所示:

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

这个方法非常简单,只有一行代码,即调用ParameterHandler中的setParameters()方法,在开始梳理这个方法前,先看一下这个接口的继承体系:

从图中可以知道ParameterHandler接口的实现类只有一个DefaultParameterHandler。下面我们一起看一下setParameter()方法的源码,具体如下所示:

public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    MetaObject metaObject = 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 {
          if (metaObject == null) {
            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);
        }
      }
    }
  }
}

这个方法是来解决sql中的占位符的,也就是用实际的数据将sql中的占位符替换掉。个人在跟踪源码时,将sql语句改了一下,得到下面这样一幅运行时图片:

总结一下,这里创建PreparedStatement命令并对命令中的占位符进行处理的操作和手动编写jdbc时的处理完全一致。但是我有个疑问,如果通过MyBatis动态条件创建的sql语句,它们中的占位符是何时被替换的呢?关于这个问题的答案,我们要回到CachingExecutor类中接收四个参数的query()方法中寻找,也就是上一小节中梳理的第六步中提到的那个方法。这个方法的源码如下所示:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
    throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这个方法的第一行代码会调用MappedStatement中的getBoundSql()方法创建一个BoundSql对象。这个方法的源码如下所示:

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

// DynamicSqlSource中的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;
}

从源码可以看出创建BoundSql的核心代码位于SqlSource的getBoundSql()方法中,这个方法会接收一个Object类型的参数,比如本例中的userDto对象,最终执行创建出来的BoundSql对象的状态如下图所示:

梳理到这里我们有必要对这个环节中出现的两个新类进行一次梳理,这两个类分别是 BoundSql和SqlSource。根据源码跟踪的结果,个人理解BoundSql是一个sql字符串的包装类。这是一个普通的java类,其中有一个String类型的名字为sql的属性(用于存储sql字符串)、一个List<ParameterMapping>类型的名字为parameterMapping的属性(用于存储参数的名称,即那些直接定义在sql语句中的没有被包装进某个参数对象中的参数名)、一个Object类型的名字为parameterObject的属性(实际的参数数据,比如案例中的%%表示字符串和UserDto对象),详情可以看下面这幅图:

SqlSource是一个表示MyBatis中不同类型的sql的数据结构,其表示的是通过MyBatis定义的动态标签在xml文件中配置出来的sql语句。我们知道在配置文件中可以配置的sql形式常用的有两种,如下图所示:

上图中的第一中sql会用SqlSource的子类DynamicSqlSource表示,而下面配置的dql语句会用RawSqlSource表示。这里我们不得不看一下SqlSource的继承结构,如下图所示:

梳理到这里我们需要谨记一点:MappedStatement会持有一个SqlSource对象,而SqlSource是程序构建BoundSql的基础。所以通过梳理我们知道在MyBatis中存在与手动jdbc编程中的创建sql语句、创建Statement命令、处理预编译命令中占位符等操作类似的处理。其中处理预编译命令中占位符的操作个人理解可以分为两种:一种是在创建BoundSql对象时直接处理掉;另一种就是在创建完预编译命令之后进行处理

好了,我们花了如此长的篇幅来介绍sql命令创建及参数预处理的过程,但本小节还有一个知识点未梳理,即查询结果集的处理。这个处理过程是在哪里呢?

下面让然我们回到第二小节第十步中提到的doQuery()方法中,该方法位于SimpleExecutor类中,其源码如下所示:

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

该方法的最后一行这样写到:handler.query(stmt, resultHandler)。注意这里的handler实际类型是RoutingStatementHandler,所以这个调用会进入到RoutingStatementHandler类的query()方法中,这个方法紧接着就把处理转交给了PreparedStatementHandler类的query()方法,该方法的源码如下所示:

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

这个方法中有这样一句ps.execute()。没错!这句就是用来执行数据库查询的。这里我们主要看resultSetHandler.handleResultSets(ps)这句代码,这不就是jdbc手动编程中处理查询结果集的操作吗?在看开始梳理这个方法的源码前,让我们一起看一下ResultSetHandler及ResultHandler的继承结构:

由图不难看出ResultSetHandler和ResultHandler都是一个接口,其中ResultSetHandler接口只有一个实现类DefaultResultSetHandler,而ResultHander接口有两个实现类,它们分别是DefaultMapResultHandler和DefaultResultHandler。下面回到resultSetHandler.handleResultSets(ps)这句代码处,然后看一下这个方法的源码,如下所示:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  final List<Object> multipleResults = new ArrayList<>();

  int resultSetCount = 0;
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
  }

  return collapseSingleResultList(multipleResults);
}

这个方法的主要处理内容就是将数据库中查询出来的数据转化为指定的格式。因为这里的处理思路清晰且逻辑相对简单,所以就不再梳理了,有兴趣的可以自己跟踪一下源码!

4 执行sql操作

在上一小节中我们梳理了sql语句及sql执行结果的处理,这一小节我们看一下sql命令的执行。其实很简单,与jdbc手动编程中的写法并无区别。在上一小节的末尾,我们提到了PreparedStatementHandler类的query()方法,该方法中有这样一句代码ps.execute()。看到这我们还有上面理由不开心呢?下面就对比一下jdbc手动编程和MyBatis框架执行数据库命令的写法,详情参见下图:

5 MyBatis拦截器的执行时机

《MyBatis是纸老虎吗?(四)》这篇文章梳理了MyBatis拦截器相关的知识,通过梳理我们知道如何在MyBatis中定义一个拦截器,以及如何向MyBatis配置文件中添加一个拦截器。虽然当时也跟踪了拦截器的执行,但当时也仅知道这样做之后,拦截器会生效,至于拦截器执行时机究竟在哪里,我并不十分清楚。因此在本篇文章中,我想深入研究一下拦截器的定义细节和执行时机。这里我直接跳过拦截器定义和添加,直接进入执行缓解,并以问题为导向展开梳理。这个执行步骤与本篇第三小节梳理的步骤一致,在执行到第五步时,运行时图片如下所示:

这段代码执行到这里会调用DefaultSqlSession对象上的executor对象中的query()方法开始下一步逻辑的执行。这里一定要注意:这个executor对象是一个代理对象,所以调用后会直接进入到我们自定义的ExamplePlugin插件的intercept()方法中。详情请参见下面这幅图:

图中蓝色那一行会直接调用Invocation类上的proceed()方法,这会触发后续逻辑的执行。注意Invocation类持有一个目标对象、目标方法及其参数。所以如果相对方法参数进行一些处理,在拦截器中是做得到的。

6 总结

每每这时,我都会让自己放空一会,考虑一下自己通过梳理学到了什么,以及在梳理过程中遗漏了哪些重要的点。这次也不例外,通过这篇文章的梳理,我对MyBatis的执行流程有了更加全面的认知,不再局限于之前篇幅时的解析过程,那这次梳理究竟给我带来了什么呢?

首先通过这次梳理,我解决了《MyBatis是纸老虎吗?(二)》这篇文章中遗留的一个问题:Transaction和SqlSession之间存在着怎样的关系?那这两者之间究竟有着什么样的关系呢?首先SqlSession是MyBatis定义的一个接口,其中定义了很多数据库操作,比如数据查询操作、数据更新操作、数据删除操作等等。而Transaction是MyBatis定义的一个管理与数据库交互的连接的接口,这个接口中有这样几个操作:获取数据库连接、提交事务、回滚事务、关闭数据库连接等。Transaction对象被包装进Executor对象中,而Executor对象又被包装金SqlSession对象中,所以SqlSession通过Executor间接拥有一个Transaction属性。这样后面通过SqlSession对象进行查询、更新、删除等操作时,可以很方便的通过Transaction对象拿到一个数据库连接,然后对数据库进行操作。

接着通过这次梳理,我对MyBatis的执行过程有了更加微观的认识。首先MyBatis对数据库的操作与手动编写jdbc方式的操作没有任何区别。使用MyBatis的好处是可以利用框架约定的规则让我们专注于与业务有关的sql编写,因为MyBatis框架帮助开发者解决了很多繁琐的操作,比如频繁的数据库连接建立、释放等管理操作、频繁的数据库数据与java对象之间的转换操作。不过MyBatis框架兼顾到了开发者的一些特殊需求,通过动态代理方式向外提供了拦截器,这样开发者可以利用拦截器对sql语句、sql语句查询结果、sql语句参数等组件做一些特殊处理,比如通过拦截器,我们可以实现sql执行耗时统计、为查询数据进行加解密操作、为查询语句新增分页命令等等。不过要使用这个功能就需要对MyBatis提供的拦截器有足够深入的了解。首先Interceptor是MyBatis提供的自定义插件的组件,即开发者可以通过实现该接口自定义插件;接着MyBatis提供了@Interceptors和@Signature注解帮助开发者细化自定义拦截器可以拦截的方法,其中@Signature注解可以指定要拦截的方法的名称、方法的参数以及方法所属的接口(这个值可以有四个:Executor、ParameterHandler、ResultHandler、StatementHandler);再次Invocation是一个包装类,其也是Interceptor接口中名为interceptor()方法的参数,这个类中包含了这样一些属性:目标对象、目标方法、目标方法参数,也就是说通过这个对象开发者可以拿到这个代理对象要代理的目标对象、方法及方法参数,然后对这些信息做特殊处理。相信大家都用过MyBatis分页插件,我私下跟踪时用到的分页插件就是这么做的(另外公司项目中用到的sql耗时统计插件也是这么实现的),下面这幅图展示了MyBatis插件的实现过程

如果没记错上面关于Interceptor的梳理在《MyBatis是纸老虎吗?(四)》这篇文章中已经介绍过。那我对MyBatis更加微观的认识又体现在哪里呢?首先MyBatis为了实现更大的灵活性提供很多组件,比如MappedStatement、SqlSource、BoundSql、Executor、ParameterHandler、ResultHandler以及StatementHandler。其中MappedStatement对象用于存储一个sql命令,这个对象会包含一个SqlSource对象,注意这个MappedStatement对象会存储到Configuration对象中。SqlSource是创建一个sql命令字符串原材料,这要怎么理解呢?大家都知道MyBatis提供了很多定义一个sql语句的xml标签吧,比如insert、select、delete、update、sql、parameterMap等等,这些数据解析后会用SqlSource表示,之后通过调用MappedStatement对象上的getBoundSql()方法,该方法会接受一个Object类型的参数值对象,比如本篇案例中提到的UserDto对象,会创建一个BoundSql对象。BoundSql代表一个基本语法合规,且可以运行的sql命令字符串,可以将其理解成半成品,虽然不能立马投入使用,但已经初具形态,只不过需要再加工一下,这个加工就是本篇第三小节介绍的SQL语句对象的处理,主要是对sql语句中的一些占位符进行处理(个人理解)。Executor是实际调度sql语句执行者的地方,为什么这么说,因为Executor也不是实际执行sql的地方(实际的执行操作在StatementHandler类中),不过它比SqlSession更加靠近执行层,利用地球分层理论来理解,StatementHandler在地心一层,Executor位于地幔一层,SqlSession则位于地表一层。既然这里提到了StatementHandler,那就说说这个组件吧,这个组件对手动jdbc编程中Statement命令的创建过程、执行过程进行了包装,通过它们我们可以创建一个PreparedStatement命令,然后对该命令中的占位符进行处理,最后执行数据库操作。ResultHandler和ParameterHandler由于在跟踪中没有用到,所以这里就不再赘述了,不过个人理解它们分别对数据库处理结果和sql命令参数做处理。

  • 25
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器挖掘工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值