背景

本文基于调试模式下的单步运行记录,通过分析一个查询订单协议的操作,侧面剖析 iBATIS 框架的执行动作与原理。

一、核心组件简介

1. DAO 层与 SqlMapClientDaoSupport

DAL 层的 DAO 接口实现类通常会继承 SqlMapClientDaoSupport。Spring 容器在初始化一个 DAO Bean 实例时,通常会注入两块信息:DataSource(数据源)和 SqlMapClient(主要是 SQL 语句)。这两块信息会被封装到 SqlMapClientTemplate 中。

2. 数据源配置 (DataSource)

其中数据源的实例通常采用 Apache 的开源项目 DBCP。代码配置如下:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="xxxx"/>
    <property name="username"><value>xxxx</value></property>
    <property name="password"><value>xxxxx</value></property>
    <property name="maxActive"><value>20</value></property>
    <property name="initialSize"><value>1</value></property>
    <property name="maxWait"><value>60000</value></property>
    <property name="maxIdle"><value>20</value></property>
    <property name="minIdle"><value>3</value></property>
    <property name="removeAbandoned"><value>true</value></property>
    <property name="removeAbandonedTimeout"><value>180</value></property>
    <property name="connectionProperties"><value>clientEncoding=GBK</value></property>
</bean>

各配置参数的具体含义可参照:dbcp 基本配置和重连配置

3. SqlMapClient 配置与初始化

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation">
        <value>classpath:sqlmap.xml</value>
    </property>
</bean>

<bean id="sqlMapClientTddl" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="dataSource" ref="tGroupDataSource"/>
    <property name="configLocation" value="classpath:sqlmap.xml"/>
</bean>

SqlMapClient 主要是借助于实现 FactoryBeanInitializingBean 两个接口,加载 sql.xml 文件资源信息,得到 SqlMapClient 组件。

:上面的 sqlMapClient 默认不配置数据源,后面的 SqlMapClientTemplate 优先从全局变量中取,如果没有再从 sqlMapClient 中查找。
public DataSource getDataSource() {  
    DataSource ds = super.getDataSource();  
    return (ds != null ? ds : this.sqlMapClient.getDataSource());  
}

构造 SqlMapClient 组件的核心代码块:

public void afterPropertiesSet() throws Exception {  
    if (this.lobHandler != null) {  
        // Make given LobHandler available for SqlMapClient configuration.  
        // Do early because because mapping resource might refer to custom types.  
        configTimeLobHandlerHolder.set(this.lobHandler);  
    }  

    try {  
        this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);  

        // Tell the SqlMapClient to use the given DataSource, if any.  
        if (this.dataSource != null) {  
            TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass.newInstance();  
            DataSource dataSourceToUse = this.dataSource;  
            if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {  
                dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);  
            }  
            transactionConfig.setDataSource(dataSourceToUse);  
            transactionConfig.initialize(this.transactionConfigProperties);  
            applyTransactionConfig(this.sqlMapClient, transactionConfig);  
        }  
    }  
    finally {  
        if (this.lobHandler != null) {  
            // Reset LobHandler holder.  
            configTimeLobHandlerHolder.set(null);  
        }  
    }  
}

4. 核心接口与类

  1. SqlMapExecutor
    该接口是对 SQL 操作行为的抽象,提供了 SQL 单条执行和批处理涉及的所有操作方法。
  2. SqlMapTransactionManager
    该接口是对事务行为的抽象,提供了事务执行过程中涉及的所有方法。
  3. SqlMapClient
    该接口定位是 SQL 执行客户端,是线程安全的,用于处理多个线程的 SQL 执行。它继承了上面两个接口,这意味着该接口具有 SQL 执行和事务处理的能力,该接口的核心实现类是 SqlMapClientImpl
  4. SqlMapSession
    该接口在继承关系上和 SqlMapClient 一致,但它的定位是保存单线程 SQL 执行过程的 Session 信息。该接口的核心实现类是 SqlMapSessionImpl
  5. MappedStatement
    该接口定位是单条 SQL 执行时的上下文环境信息,如 SQL 标识、SQL、参数信息、返回结果、操作行为等。
  6. ParameterMap / ResultMap
    该接口用于在 SQL 执行的前后提供参数准备和执行结果集的处理。

整体类图:

二、核心调用流程分析

接下来到了数据持久层的代码调用,所有的数据库 DML 操作(增、删、改、查)都是借助于 SqlMapClientTemplate 来实现的。

1. 单条查询示例

public OrderEnsureProtocolDO getOrderEnsureProtocolByOrderId(Long orderId) {  
    if (orderId == null) {  
        return null;  
    }  
    return (OrderEnsureProtocolDO) this.getSqlMapClientTemplate().queryForObject(
        "MS-FIND-ORDERENSUREPROTOCOL-BY-ORDERID", orderId);  
}

2. 批处理示例

如果一次执行的 SQL 较多,我们会采用批处理的形式:

public void batchDeleteOfferSaleRecord(final List<Long> orderEntryIds) throws Exception {  
    if (orderEntryIds == null || orderEntryIds.size() < 1 || orderEntryIds.size() > 50) {  
        return;  
    }  

    this.getSqlMapClientTemplate().execute(new SqlMapClientCallback() {  
        @Override  
        public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {  
            executor.startBatch();  
            for (Long entryId : orderEntryIds) {  
                executor.insert(MS_DELETE_SALE_RECORD, entryId);  
            }  
            return executor.executeBatch();  
        }  
    });  
}

3. 内部执行流程 (execute)

不管采用上面哪种方式,查看源代码会发现,最后都是在调用 execute(SqlMapClientCallback action) 方法。

queryForObject 内部封装:

public Object queryForObject(final String statementName, final Object parameterObject) throws DataAccessException {  
    return execute(new SqlMapClientCallback() {  
        public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {  
            return executor.queryForObject(statementName, parameterObject);  
        }  
    });  
}

核心 execute 方法:

public Object execute(SqlMapClientCallback action) throws DataAccessException {  
    Assert.notNull(action, "Callback object must not be null");  
    Assert.notNull(this.sqlMapClient, "No SqlMapClient specified");  
    // 获取 session 信息 (SqlMapSessionImpl 实例)  
    SqlMapSession session = this.sqlMapClient.openSession();  
    if (logger.isDebugEnabled()) {  
        logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation");  
    }  
    Connection ibatisCon = null;  

    try {  
        Connection springCon = null; // 数据库连接  
        DataSource dataSource = getDataSource();  
        boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);  

        try {  
            // 获取数据连接  
            ibatisCon = session.getCurrentConnection();  
            if (ibatisCon == null) {  
                springCon = (transactionAware ?  
                        dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));  
                // 将数据源 set 到 session 会话中  
                session.setUserConnection(springCon);  
                if (logger.isDebugEnabled()) {  
                    logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation");  
                }  
            }  
            else {  
                if (logger.isDebugEnabled()) {  
                    logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation");  
                }  
            }  
        }  
        catch (SQLException ex) {  
            throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);  
        }  
        try {  
            // 执行 SQL    
            return action.doInSqlMapClient(session);  
        }  
        catch (SQLException ex) {  
            throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);  
        }  
        finally {  
            // 省略。。。一系列的关闭工作  
        }  
    }  
}

4. 深层调用链 (Delegate -> MappedStatement -> SqlExecutor)

SqlMapSessionImpl().queryForObject() 的方法很简单,直接交给代理对象 SqlMapExecutorDelegate 处理(里面注入了很多功能对象,负责具体的 SQL 执行):

public Object queryForObject(String id, Object paramObject) throws SQLException {  
    return delegate.queryForObject(session, id, paramObject);  
}

经过 N 层重载,最后调用内部的通用方法:

// 入参:   
// id = MS-FIND-ORDERENSUREPROTOCOL-BY-ORDERID   
// paramObject = 26749329

public Object queryForObject(SessionScope session, String id, Object paramObject, Object resultObject) throws SQLException {  
    Object object = null;  

    // MappedStatement 对象集是上文中提及的初始化方法 SqlMapClientFactoryBean.afterPropertiesSet() 中,由配置文件构建而成  
    // 调试中的 ms 为 SelectStatement,具体的执行器  
    MappedStatement ms = getMappedStatement(id);    
    // 用于事务执行  
    Transaction trans = getTransaction(session);    
    boolean autoStart = trans == null;  

    try {  
        trans = autoStartTransaction(session, autoStart, trans);  
        // 从 RequestScope 池中获取该次 SQL 执行中的上下文环境 RequestScope    
        RequestScope request = popRequest(session, ms);     
        try {  
            // 执行 SQL   
            object = ms.executeQueryForObject(request, trans, paramObject, resultObject);     
        } finally {  
            pushRequest(request);  // 归还 RequestScope  
        }  

        autoCommitTransaction(session, autoStart);  
    } finally {  
        autoEndTransaction(session, autoStart);  
    }  

    return object;  
}

接下来由 MappedStatement.executeQueryForObject() 来执行:

public Object executeQueryForObject(RequestScope request, Transaction trans, Object parameterObject, Object resultObject) throws SQLException {  
    try {  
        Object object = null;  

        DefaultRowHandler rowHandler = new DefaultRowHandler();  
        // 执行 SQL 语句  
        executeQueryWithCallback(request, trans.getConnection(), parameterObject, resultObject, rowHandler, SqlExecutor.NO_SKIPPED_RESULTS, SqlExecutor.NO_MAXIMUM_RESULTS);  

        // 结果处理,返回结果  
        List list = rowHandler.getList();    
        if (list.size() > 1) {  
            throw new SQLException("Error: executeQueryForObject returned too many results.");  
        } else if (list.size() > 0) {  
            object = list.get(0);  
        }  
        // 。。。。。。。。。  
    }  
}

MappedStatement.executeQueryWithCallback() 方法包含了参数值映射、SQL 准备和 SQL 执行等关键过程:

protected void executeQueryWithCallback(RequestScope request, Connection conn, Object parameterObject, Object resultObject, RowHandler rowHandler, int skipResults, int maxResults) throws SQLException {  
    // 预先封装错误信息,如果报错时便于排查问题  
    ErrorContext errorContext = request.getErrorContext();  
    errorContext.setActivity("preparing the mapped statement for execution");  
    errorContext.setObjectId(this.getId());  
    errorContext.setResource(this.getResource());  
    try {  
        // 验证入参  
        parameterObject = validateParameter(parameterObject);     

        // 获取 SQL 对象  
        Sql sql = getSql();     

        errorContext.setMoreInfo("Check the parameter map.");  
        // 入参映射  
        ParameterMap parameterMap = sql.getParameterMap(request, parameterObject);  

        errorContext.setMoreInfo("Check the result map.");  
        // 获取结果  
        ResultMap resultMap = sql.getResultMap(request, parameterObject);    

        request.setResultMap(resultMap);  
        request.setParameterMap(parameterMap);  

        errorContext.setMoreInfo("Check the parameter map.");  
        // 获取参数值  
        Object[] parameters = parameterMap.getParameterObjectValues(request, parameterObject);     

        errorContext.setMoreInfo("Check the SQL statement.");  
        // 获取拼装后的 SQL 语句  
        String sqlString = sql.getSql(request, parameterObject);     

        errorContext.setActivity("executing mapped statement");  
        errorContext.setMoreInfo("Check the SQL statement or the result map.");  
        RowHandlerCallback callback = new RowHandlerCallback(resultMap, resultObject, rowHandler);  
        // SQL 执行  
        sqlExecuteQuery(request, conn, sqlString, parameters, skipResults, maxResults, callback);    

        // ....省略  
    }  
}

最后调用 com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery

public void executeQuery(RequestScope request, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException {  
    // ...省略  
    PreparedStatement ps = null;  
    ResultSet rs = null;  
    setupResultObjectFactory(request);  
    try {  
        errorContext.setMoreInfo("Check the SQL Statement (preparation failed).");  
        Integer rsType = request.getStatement().getResultSetType();  
        // 初始化 PreparedStatement,设置 SQL、参数值等  
        if (rsType != null) {  
            ps = prepareStatement(request.getSession(), conn, sql, rsType);  
        } else {  
            ps = prepareStatement(request.getSession(), conn, sql);  
        }  
        setStatementTimeout(request.getStatement(), ps);  
        Integer fetchSize = request.getStatement().getFetchSize();  
        if (fetchSize != null) {  
            ps.setFetchSize(fetchSize.intValue());  
        }  
        errorContext.setMoreInfo("Check the parameters (set parameters failed).");  
        request.getParameterMap().setParameters(request, ps, parameters);  
        errorContext.setMoreInfo("Check the statement (query failed).");  
        // 执行  
        ps.execute();  
        errorContext.setMoreInfo("Check the results (failed to retrieve results).");  

        // 结果集处理  
        rs = handleMultipleResults(ps, request, skipResults, maxResults, callback);  

        // 。。。省略  
    }  
}

说明

版本时效性说明:本文基于 iBATIS 框架进行分析。iBATIS 项目已于 2010 年更名为 MyBatis,且 Spring 对 iBATIS 的支持类(如 SqlMapClientDaoSupport)在现代 Spring 版本中已废弃。文中涉及的配置与源码逻辑适用于旧版 iBATIS 2.x 环境,新开发项目建议直接使用 MyBatis 或 MyBatis-Plus。