1. 程式人生 > >Mybaits 原始碼解析 (六)----- 全網最詳細:Select 語句的執行過程分析(上篇)(Mapper方法是如何呼叫到XML中的SQL的?)

Mybaits 原始碼解析 (六)----- 全網最詳細:Select 語句的執行過程分析(上篇)(Mapper方法是如何呼叫到XML中的SQL的?)

上一篇我們分析了Mapper介面代理類的生成,本篇接著分析是如何呼叫到XML中的SQL

我們回顧一下MapperMethod 的execute方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    
    // 根據 SQL 型別執行相應的資料庫操作
    switch (command.getType()) {
        case INSERT: {
            // 對使用者傳入的引數進行轉換,下同
            Object param = method.convertArgsToSqlCommandParam(args);
            // 執行插入操作,rowCountResult 方法用於處理返回值
            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:
            // 根據目標方法的返回型別進行相應的查詢操作
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                // 執行查詢操作,並返回多個結果 
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                // 執行查詢操作,並將結果封裝在 Map 中返回
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                // 執行查詢操作,並返回一個 Cursor 物件
                result = executeForCursor(sqlSession, args);
            } else {
                Object param = method.convertArgsToSqlCommandParam(args);
                // 執行查詢操作,並返回一個結果
                result = sqlSession.selectOne(command.getName(), param);
            }
            break;
        case FLUSH:
            // 執行重新整理操作
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
}

selectOne 方法分析

本節選擇分析 selectOne 方法,主要是因為 selectOne 在內部會呼叫 selectList 方法。同時分析 selectOne 方法等同於分析 selectList 方法。程式碼如下

// 執行查詢操作,並返回一個結果
result = sqlSession.selectOne(command.getName(), param);

我們看到是通過sqlSession來執行查詢的,並且傳入的引數為command.getName()和param,也就是namespace.methodName(mapper.EmployeeMapper.getAll)和方法的執行引數。我們知道了,所有的資料庫操作都是交給sqlSession來執行的,那我們就來看看sqlSession的方法

DefaultSqlSession

public <T> T selectOne(String statement, Object parameter) {
    // 呼叫 selectList 獲取結果
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
        // 返回結果
        return list.get(0);
    } else if (list.size() > 1) {
        // 如果查詢結果大於1則丟擲異常
        throw new TooManyResultsException(
            "Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

如上,selectOne 方法在內部呼叫 selectList 了方法,並取 selectList 返回值的第1個元素作為自己的返回值。如果 selectList 返回的列表元素大於1,則丟擲異常。下面我們來看看 selectList 方法的實現。

DefaultSqlSession

private final Executor executor;
public <E> List<E> selectList(String statement, Object parameter) {
    // 呼叫過載方法
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 通過MappedStatement的Id獲取 MappedStatement
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 呼叫 Executor 實現類中的 query 方法
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

我們之前建立DefaultSqlSession的時候,是建立了一個Executor的例項作為其屬性的,我們看到通過MappedStatement的Id獲取 MappedStatement後,就交由Executor去執行了

我們回顧一下前面的文章,Executor的建立過程,程式碼如下

//建立一個執行器,預設是SIMPLE
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //根據executorType來建立相應的執行器,Configuration預設是SIMPLE
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      //建立SimpleExecutor例項,並且包含Configuration和transaction屬性
      executor = new SimpleExecutor(this, transaction);
    }
    
    //如果要求快取,生成另一種CachingExecutor,裝飾者模式,預設都是返回CachingExecutor
    /**
     * 二級快取開關配置示例
     * <settings>
     *   <setting name="cacheEnabled" value="true"/>
     * </settings>
     */
    if (cacheEnabled) {
      //CachingExecutor使用裝飾器模式,將executor的功能新增上了二級快取的功能,二級快取會單獨文章來講
      executor = new CachingExecutor(executor);
    }
    //此處呼叫外掛,通過外掛可以改變Executor行為,此處我們後面單獨文章講
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

executor包含了Configuration和Transaction,預設的執行器為SimpleExecutor,如果開啟了二級快取(預設開啟),則CachingExecutor會包裝SimpleExecutor,那麼我們該看CachingExecutor的query方法了

CachingExecutor

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 獲取 BoundSql
    BoundSql boundSql = ms.getBoundSql(parameterObject);
   // 建立 CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 呼叫過載方法
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上面的程式碼用於獲取 BoundSql 物件,建立 CacheKey 物件,然後再將這兩個物件傳給過載方法。CacheKey 以及接下來即將出現的一二級快取將會獨立成文進行分析。

獲取 BoundSql

我們先來看看獲取BoundSql

// 獲取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);

呼叫了MappedStatement的getBoundSql方法,並將執行時引數傳入其中,我們大概的猜一下,這裡是不是拼接SQL語句呢,並將執行時引數設定到SQL語句中?

我們都知道 SQL 是配置在對映檔案中的,但由於對映檔案中的 SQL 可能會包含佔位符 #{},以及動態 SQL 標籤,比如 <if>、<where> 等。因此,我們並不能直接使用對映檔案中配置的 SQL。MyBatis 會將對映檔案中的 SQL 解析成一組 SQL 片段。我們需要對這一組片段進行解析,從每個片段物件中獲取相應的內容。然後將這些內容組合起來即可得到一個完成的 SQL 語句,這個完整的 SQL 以及其他的一些資訊最終會儲存在 BoundSql 物件中。下面我們來看一下 BoundSql 類的成員變數資訊,如下:

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

下面用一個表格列舉各個成員變數的含義。

變數名型別用途
sql String 一個完整的 SQL 語句,可能會包含問號 ? 佔位符
parameterMappings List 引數對映列表,SQL 中的每個 #{xxx} 佔位符都會被解析成相應的 ParameterMapping 物件
parameterObject Object 執行時引數,即使用者傳入的引數,比如 Article 物件,或是其他的引數
additionalParameters Map 附加引數集合,用於儲存一些額外的資訊,比如 datebaseId 等
metaParameters MetaObject additionalParameters 的元資訊物件

接下來我們接著MappedStatement 的 getBoundSql 方法,程式碼如下:

public BoundSql getBoundSql(Object parameterObject) {

    // 呼叫 sqlSource 的 getBoundSql 獲取 BoundSql,把method執行時引數傳進去
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);return boundSql;
}

MappedStatement 的 getBoundSql 在內部呼叫了 SqlSource 實現類的 getBoundSql 方法,並把method執行時引數傳進去,SqlSource 是一個介面,它有如下幾個實現類:

  • DynamicSqlSource
  • RawSqlSource
  • StaticSqlSource
  • ProviderSqlSource
  • VelocitySqlSource

當 SQL 配置中包含 ${}(不是 #{})佔位符,或者包含 <if>、<where> 等標籤時,會被認為是動態 SQL,此時使用 DynamicSqlSource 儲存 SQL 片段。否則,使用 RawSqlSource 儲存 SQL 配置資訊。我們來看看DynamicSqlSource的getBoundSql

DynamicSqlSource

public BoundSql getBoundSql(Object parameterObject) {
    // 建立 DynamicContext
    DynamicContext context = new DynamicContext(configuration, parameterObject);

    // 解析 SQL 片段,並將解析結果儲存到 DynamicContext 中,這裡會將${}替換成method對應的執行時引數,也會解析<if><where>等SqlNode
    rootSqlNode.apply(context);
    
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    /*
     * 構建 StaticSqlSource,在此過程中將 sql 語句中的佔位符 #{} 替換為問號 ?,
     * 併為每個佔位符構建相應的 ParameterMapping
     */
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    
 // 呼叫 StaticSqlSource 的 getBoundSql 獲取 BoundSql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

    // 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
        boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
}

該方法由數個步驟組成,這裡總結一下:

  1. 建立 DynamicContext
  2. 解析 SQL 片段,並將解析結果儲存到 DynamicContext 中
  3. 解析 SQL 語句,並構建 StaticSqlSource
  4. 呼叫 StaticSqlSource 的 getBoundSql 獲取 BoundSql
  5. 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql

DynamicContext

DynamicContext 是 SQL 語句構建的上下文,每個 SQL 片段解析完成後,都會將解析結果存入 DynamicContext 中。待所有的 SQL 片段解析完畢後,一條完整的 SQL 語句就會出現在 DynamicContext 物件中。

public class DynamicContext {

    public static final String PARAMETER_OBJECT_KEY = "_parameter";
    public static final String DATABASE_ID_KEY = "_databaseId";

    //bindings 則用於儲存一些額外的資訊,比如執行時引數
    private final ContextMap bindings;
    //sqlBuilder 變數用於存放 SQL 片段的解析結果
    private final StringBuilder sqlBuilder = new StringBuilder();

    public DynamicContext(Configuration configuration, Object parameterObject) {
        // 建立 ContextMap,並將執行時引數放入ContextMap中
        if (parameterObject != null && !(parameterObject instanceof Map)) {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            bindings = new ContextMap(metaObject);
        } else {
            bindings = new ContextMap(null);
        }

        // 存放執行時引數 parameterObject 以及 databaseId
        bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
        bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
    }

    
    public void bind(String name, Object value) {
        this.bindings.put(name, value);
    }

    //拼接Sql片段
    public void appendSql(String sql) {
        this.sqlBuilder.append(sql);
        this.sqlBuilder.append(" ");
    }
    
    //得到sql字串
    public String getSql() {
        return this.sqlBuilder.toString().trim();
    }

    //繼承HashMap
    static class ContextMap extends HashMap<String, Object> {

        private MetaObject parameterMetaObject;

        public ContextMap(MetaObject parameterMetaObject) {
            this.parameterMetaObject = parameterMetaObject;
        }

        @Override
        public Object get(Object key) {
            String strKey = (String) key;
            // 檢查是否包含 strKey,若包含則直接返回
            if (super.containsKey(strKey)) {
                return super.get(strKey);
            }

            if (parameterMetaObject != null) {
                // 從執行時引數中查詢結果,這裡會在${name}解析時,通過name獲取執行時引數值,替換掉${name}字串
                return parameterMetaObject.getValue(strKey);
            }

            return null;
        }
    }
    // 省略部分程式碼
}

解析 SQL 片段

接著我們來看看解析SQL片段的邏輯

rootSqlNode.apply(context);

對於一個包含了 ${} 佔位符,或 <if>、<where> 等標籤的 SQL,在解析的過程中,會被分解成多個片段。每個片段都有對應的型別,每種型別的片段都有不同的解析邏輯。在原始碼中,片段這個概念等價於 sql 節點,即 SqlNode。

StaticTextSqlNode 用於儲存靜態文字,TextSqlNode 用於儲存帶有 ${} 佔位符的文字,IfSqlNode 則用於儲存 <if> 節點的內容。MixedSqlNode 內部維護了一個 SqlNode 集合,用於儲存各種各樣的 SqlNode。接下來,我將會對 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等進行分析

public class MixedSqlNode implements SqlNode {
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 遍歷 SqlNode 集合
        for (SqlNode sqlNode : contents) {
            // 呼叫 salNode 物件本身的 apply 方法解析 sql
            sqlNode.apply(context);
        }
        return true;
    }
}

MixedSqlNode 可以看做是 SqlNode 實現類物件的容器,凡是實現了 SqlNode 介面的類都可以儲存到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 邏輯比較簡單,即遍歷 SqlNode 集合,並呼叫其他 SqlNode實現類物件的 apply 方法解析 sql。

StaticTextSqlNode

public class StaticTextSqlNode implements SqlNode {

    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    @Override
    public boolean apply(DynamicContext context) {
        //直接拼接當前sql片段的文字到DynamicContext的sqlBuilder中
        context.appendSql(text);
        return true;
    }
}

StaticTextSqlNode 用於儲存靜態文字,直接將其儲存的 SQL 的文字值拼接到 DynamicContext 的sqlBuilder中即可。下面分析一下 TextSqlNode。

TextSqlNode

public class TextSqlNode implements SqlNode {

    private final String text;
    private final Pattern injectionFilter;

    @Override
    public boolean apply(DynamicContext context) {
        // 建立 ${} 佔位符解析器
        GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
        // 解析 ${} 佔位符,通過ONGL 從使用者傳入的引數中獲取結果,替換text中的${} 佔位符
        // 並將解析結果的文字拼接到DynamicContext的sqlBuilder中
        context.appendSql(parser.parse(text));
        return true;
    }

    private GenericTokenParser createParser(TokenHandler handler) {
        // 建立佔位符解析器
        return new GenericTokenParser("${", "}", handler);
    }

    private static class BindingTokenParser implements TokenHandler {

        private DynamicContext context;
        private Pattern injectionFilter;

        public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
            this.context = context;
            this.injectionFilter = injectionFilter;
        }

        @Override
        public String handleToken(String content) {
            Object parameter = context.getBindings().get("_parameter");
            if (parameter == null) {
                context.getBindings().put("value", null);
            } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                context.getBindings().put("value", parameter);
            }
            // 通過 ONGL 從使用者傳入的引數中獲取結果
            Object value = OgnlCache.getValue(content, context.getBindings());
            String srtValue = (value == null ? "" : String.valueOf(value));
            // 通過正則表示式檢測 srtValue 有效性
            checkInjection(srtValue);
            return srtValue;
        }
    }
}

GenericTokenParser 是一個通用的標記解析器,用於解析形如 ${name},#{id} 等標記。此時是解析 ${name}的形式,從執行時引數的Map中獲取到key為name的值,直接用執行時引數替換掉 ${name}字串,將替換後的text字串拼接到DynamicContext的sqlBuilder中

舉個例子吧,比喻我們有如下SQL

SELECT * FROM user WHERE name = '${name}' and id= ${id}

假如我們傳的引數 Map中name值為 chenhao,id為1,那麼該 SQL 最終會被解析成如下的結果:

SELECT * FROM user WHERE name = 'chenhao' and id= 1

很明顯這種直接拼接值很容易造成SQL注入,假如我們傳入的引數為name值為 chenhao'; DROP TABLE user;#  ,解析得到的結果為

SELECT * FROM user WHERE name = 'chenhao'; DROP TABLE user;#'

由於傳入的引數沒有經過轉義,最終導致了一條 SQL 被惡意引數拼接成了兩條 SQL。這就是為什麼我們不應該在 SQL 語句中是用 ${} 佔位符,風險太大。接著我們來看看IfSqlNode

IfSqlNode

public class IfSqlNode implements SqlNode {

    private final ExpressionEvaluator evaluator;
    private final String test;
    private final SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
        this.evaluator = new ExpressionEvaluator();
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 通過 ONGL 評估 test 表示式的結果
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            // 若 test 表示式中的條件成立,則呼叫其子節點節點的 apply 方法進行解析
            // 如果是靜態SQL節點,則會直接拼接到DynamicContext中
            contents.apply(context);
            return true;
        }
        return false;
    }
}

IfSqlNode 對應的是 <if test='xxx'> 節點,首先是通過 ONGL 檢測 test 表示式是否為 true,如果為 true,則呼叫其子節點的 apply 方法繼續進行解析。如果子節點是靜態SQL節點,則子節點的文字值會直接拼接到DynamicContext中

好了,其他的SqlNode我就不一一分析了,大家有興趣的可以去看看

解析 #{} 佔位符

經過前面的解析,我們已經能從 DynamicContext 獲取到完整的 SQL 語句了。但這並不意味著解析過程就結束了,因為當前的 SQL 語句中還有一種佔位符沒有處理,即 #{}。與 ${} 佔位符的處理方式不同,MyBatis 並不會直接將 #{} 佔位符替換為相應的引數值,而是將其替換成?。其解析是在如下程式碼中實現的

SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

我們看到將前面解析過的sql字串和執行時引數的Map作為引數,我們來看看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 = parser.parse(originalSql);
    // 封裝解析結果到 StaticSqlSource 中,並返回,因為所有的動態引數都已經解析了,可以封裝成一個靜態的SqlSource
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

public String handleToken(String content) {
    // 獲取 content 的對應的 ParameterMapping
    parameterMappings.add(buildParameterMapping(content));
    // 返回 ?
    return "?";
}

我們看到將Sql中的 #{} 佔位符替換成"?",並且將對應的引數轉化成ParameterMapping 物件,通過buildParameterMapping 完成,最後建立一個StaticSqlSource,將sql字串和ParameterMappings為引數傳入,返回這個StaticSqlSource

private ParameterMapping buildParameterMapping(String content) {
    /*
     * 將#{xxx} 佔位符中的內容解析成 Map。
     *   #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
     *      上面佔位符中的內容最終會被解析成如下的結果:
     *  {
     *      "property": "age",
     *      "typeHandler": "MyTypeHandler", 
     *      "jdbcType": "NUMERIC", 
     *      "javaType": "int"
     *  }
     */
    Map<String, String> propertiesMap = parseParameterMapping(content);
    String property = propertiesMap.get("property");
    Class<?> propertyType;
    // metaParameters 為 DynamicContext 成員變數 bindings 的元資訊物件
    if (metaParameters.hasGetter(property)) {
        propertyType = metaParameters.getGetterType(property);
    
    /*
     * parameterType 是執行時引數的型別。如果使用者傳入的是單個引數,比如 Employe 物件,此時 
     * parameterType 為 Employe.class。如果使用者傳入的多個引數,比如 [id = 1, author = "chenhao"],
     * MyBatis 會使用 ParamMap 封裝這些引數,此時 parameterType 為 ParamMap.class。
     */
    } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
    } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
    } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
    } else {
        /*
         * 程式碼邏輯走到此分支中,表明 parameterType 是一個自定義的類,
         * 比如 Employe,此時為該類建立一個元資訊物件
         */
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        // 檢測引數物件有沒有與 property 想對應的 getter 方法
        if (metaClass.hasGetter(property)) {
            // 獲取成員變數的型別
            propertyType = metaClass.getGetterType(property);
        } else {
            propertyType = Object.class;
        }
    }
    
    ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
    
    // 將 propertyType 賦值給 javaType
    Class<?> javaType = propertyType;
    String typeHandlerAlias = null;
    
    // 遍歷 propertiesMap
    for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
            // 如果使用者明確配置了 javaType,則以使用者的配置為準
            javaType = resolveClass(value);
            builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
            // 解析 jdbcType
            builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {...} 
        else if ("numericScale".equals(name)) {...} 
        else if ("resultMap".equals(name)) {...} 
        else if ("typeHandler".equals(name)) {
            typeHandlerAlias = value;    
        } 
        else if ("jdbcTypeName".equals(name)) {...} 
        else if ("property".equals(name)) {...} 
        else if ("expression".equals(name)) {
            throw new BuilderException("Expression based parameters are not supported yet");
        } else {
            throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
                + "}.  Valid properties are " + parameterProperties);
        }
    }
    if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
    }
    
    // 構建 ParameterMapping 物件
    return builder.build();
}

SQL 中的 #{name, ...} 佔位符被替換成了問號 ?。#{name, ...} 也被解析成了一個 ParameterMapping 物件。我們再來看一下 StaticSqlSource 的建立過程。如下:

public class StaticSqlSource implements SqlSource {

    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Configuration configuration;

    public StaticSqlSource(Configuration configuration, String sql) {
        this(configuration, sql, null);
    }

    public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.configuration = configuration;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 建立 BoundSql 物件
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    }
}

最後我們通過建立的StaticSqlSource就可以獲取BoundSql物件了,並傳入執行時引數

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

也就是呼叫上面建立的StaticSqlSource 中的getBoundSql方法,這是簡單的 return new BoundSql(configuration, sql, parameterMappings, parameterObject); ,接著看看BoundSql

public class BoundSql {
    private String sql;
    private List<ParameterMapping> parameterMappings;
   private Object parameterObject;
    private Map<String, Object> additionalParameters;
    private 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(this.additionalParameters);
    }

    public String getSql() {
        return this.sql;
    }
    //略
}

我們看到只是做簡單的賦值。BoundSql中包含了sql,#{}解析成的parameterMappings,還有執行時引數parameterObject。好了,SQL解析我們就介紹這麼多。我們先回顧一下我們程式碼是從哪裡開始的

CachingExecutor

1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
2     // 獲取 BoundSql
3     BoundSql boundSql = ms.getBoundSql(parameterObject);
4    // 建立 CacheKey
5     CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
6     // 呼叫過載方法
7     return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
8 }

如上,我們剛才都是分析的第三行程式碼,獲取到了BoundSql,CacheKey 和二級快取有關,我們留在下一篇文章單獨來講,接著我們看第七行過載方法 query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 從 MappedStatement 中獲取快取
    Cache cache = ms.getCache();
    // 若對映檔案中未配置快取或參照快取,此時 cache = null
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 若快取未命中,則呼叫被裝飾類的 query 方法,也就是SimpleExecutor的query方法
                list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 呼叫被裝飾類的 query 方法,這裡的delegate我們知道應該是SimpleExecutor
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上面的程式碼涉及到了二級快取,若二級快取為空,或未命中,則呼叫被裝飾類的 query 方法。被裝飾類為SimpleExecutor,而SimpleExecutor繼承BaseExecutor,那我們來看看 BaseExecutor 的query方法

BaseExecutor

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 從一級快取中獲取快取項,一級快取我們也下一篇文章單獨講
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 一級快取未命中,則從資料庫中查詢
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

從一級快取中查詢查詢結果。若快取未命中,再向資料庫進行查詢。至此我們明白了一級二級快取的大概思路,先從二級快取中查詢,若未命中二級快取,再從一級快取中查詢,若未命中一級快取,再從資料庫查詢資料,那我們來看看是怎麼從資料庫查詢的

BaseExecutor

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 向快取中儲存一個佔位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 呼叫 doQuery 進行查詢
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 移除佔位符
        localCache.removeObject(key);
    }
    // 快取查詢結果
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

呼叫了doQuery方法進行查詢,最後將查詢結果放入一級快取,我們來看看doQuery,在SimpleExecutor中

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
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 建立 Statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 執行查詢操作
        return handler.<E>query(stmt, resultHandler);
    } finally {
        // 關閉 Statement
        closeStatement(stmt);
    }
}

我們先來看看第一步建立StatementHandler 

建立StatementHandler 

StatementHandler有什麼作用呢?通過這個物件獲取Statement物件,然後填充執行時引數,最後呼叫query完成查詢。我們來看看其建立過程

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
    Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 建立具有路由功能的 StatementHandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 應用外掛到 StatementHandler 上
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

我們看看RoutingStatementHandler的構造方法

public class RoutingStatementHandler implements StatementHandler {

    private final StatementHandler delegate;

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

        // 根據 StatementType 建立不同的 StatementHandler 
        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());
        }
    }
    
}

RoutingStatementHandler 的構造方法會根據 MappedStatement 中的 statementType 變數建立不同的 StatementHandler 實現類。那statementType 是什麼呢?我們還要回顧一下MappedStatement 的建立過程

 

我們看到statementType 的預設型別為PREPARED,這裡將會建立PreparedStatementHandler。

接著我們看下面一行程式碼prepareStatement,

建立 Statement

建立 Statement 在 stmt = prepareStatement(handler, ms.getStatementLog()); 這句程式碼,那我們跟進去看看

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 獲取資料庫連線
    Connection connection = getConnection(statementLog);
    // 建立 Statement,
    stmt = handler.prepare(connection, transaction.getTimeout());
  // 為 Statement 設定引數
    handler.parameterize(stmt);
    return stmt;
}

在上面的程式碼中我們終於看到了和jdbc相關的內容了,建立完Statement,最後就可以執行查詢操作了。由於篇幅的原因,我們留在下一篇文章再來詳細講解

&n