MyBatis 學(xué)習(xí)筆記(八)---源碼分析篇--SQL 執(zhí)行過程詳細分析

前言

在面試中我們經(jīng)常會被到MyBatis中 #{} 占位符與${}占位符的區(qū)別。大多數(shù)的小伙伴都可以脫口而出#{} 會對值進行轉(zhuǎn)義,防止SQL注入。而${}則會原樣輸出傳入值,不會對傳入值做任何處理。本文將通過源碼層面分析為啥#{} 可以防止SQL注入。

源碼解析

首先我們來看看MyBatis 中SQL的解析過程,MyBatis 會將映射文件中的SQL拆分成一個個SQL分片段,然后在將這些分片段拼接起來。
例如:在映射文件中有如下SQL

 SELECT * FROM student
        <where>
            <if test="id!=null">
                id=${id}
            </if>
            <if test="name!=null">
                AND name =${name}
            </if>
        </where>

MyBatis 會將該SQL 拆分成如下幾部分進行解析
第一部分 SELECT * FROM Author 由StaticTextSqlNode存儲
第二部分 <where> 由WhereSqlNode 存儲
第三部分 <if></if> 由IfSqlNode存儲
第四部分 ${id} ${name} 占位符里的文本由TextSqlNode存儲。

獲取BoundSql

BoundSql 是用來存儲一個完整的SQL 語句,存儲參數(shù)映射列表以及運行時參數(shù)

public class BoundSql {

  /**
   * 一個完整的SQL語句,可能會包含問號?占位符
   */
  private String sql;
  /**
   * 參數(shù)映射列表,SQL中的每個#{xxx}
   * 占位符都會被解析成相應(yīng)的ParameterMapping對象
   */
  private List<ParameterMapping> parameterMappings;
  /**
   * 運行時參數(shù),即用戶傳入的參數(shù),比如Article對象,
   * 或是其他的參數(shù)
   */
  private Object parameterObject;
    /**
   * 附加參數(shù)集合,用戶存儲一些額外的信息,比如databaseId等
   */
  private Map<String, Object> additionalParameters;
  /**
   * additionalParameters的元信息對象
   */
  private MetaObject metaParameters;
    .... 省略部分代碼
  }

分析SQL的解析,首先從獲取BoundSql說起。其代碼源頭在MappedStatement。

  public BoundSql getBoundSql(Object parameterObject) {
	//其實就是調(diào)用sqlSource.getBoundSql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //剩下的可以暫時忽略,故省略代碼
    return boundSql;
  }

如上,可以看出其內(nèi)部就是調(diào)用的sqlSource.getBoundSql。 而我們sqlSource 接口又有如下幾個實現(xiàn)類。
DynamicSqlSource
RawSqlSource
StaticSqlSource
ProviderSqlSource
VelocitySqlSource
其中DynamicSqlSource 是對動態(tài)SQL進行解析,當(dāng)SQL配置中包含${}或者<if>,<set> 等標(biāo)簽時,會被認(rèn)定為是動態(tài)SQL,此時使用 DynamicSqlSource 存儲 SQL 片段,而RawSqlSource 是對原始的SQL 進行解析,而StaticSqlSource 是對靜態(tài)SQL進行解析。這里我們重點介紹下DynamicSqlSource。話不多說,直接看源碼。

  public BoundSql getBoundSql(Object parameterObject) {
    //生成一個動態(tài)上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
	//這里SqlNode.apply只是將${}這種參數(shù)替換掉,并沒有替換#{}這種參數(shù)
    rootSqlNode.apply(context);
	//調(diào)用SqlSourceBuilder
    SqlSourceBuilder sqlSourceParser =  new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
	//SqlSourceBuilder.parse,注意這里返回的是StaticSqlSource,解析完了就把那些參數(shù)都替換成?了,也就是最基本的JDBC的SQL寫法
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
	//看似是又去遞歸調(diào)用SqlSource.getBoundSql,其實因為是StaticSqlSource,所以沒問題,不是遞歸調(diào)用
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//  將DynamicContext的ContextMap中的內(nèi)容拷貝到BoundSql中
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

如上,該方法主要有如下幾個過程:

  1. 生成一個動態(tài)上下文
  2. 解析SQL片段,替換${}類型的參數(shù)
  3. 解析SQL語句,并將參數(shù)都替換成?
  4. 調(diào)用StaticSqlSource的getBoundSql獲取BoundSql
  5. 將DynamicContext的ContextMap中的內(nèi)容拷貝到BoundSql中。
    下面通過兩個單元測試用例理解下。
  @Test
  public void shouldMapNullStringsToNotEmptyStrings() {
    final String expected = "id=${id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=12", sql);
  }
 
    @Test
  public void shouldMapNullStringsToJINHAOEmptyStrings() {
    final String expected = "id=#{id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=?", sql);
  }

如上,${} 占位符經(jīng)過DynamicSqlSource的getBoundSql 方法之后直接替換成立用戶傳入值,而#{} 占位符則僅僅只是只會被替換成?號,不會被設(shè)值。

DynamicContext

DynamicContext 是SQL語句的上下文,每個SQL片段解析完成之后會存入DynamicContext中。讓我們來看看DynamicContext的相關(guān)代碼。

  public DynamicContext(Configuration configuration, Object parameterObject) {
	//絕大多數(shù)調(diào)用的地方parameterObject為null
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      //如果不是map型
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
	//存儲額外信息,如databaseId
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

如上,在DynamicContext的構(gòu)造函數(shù)中,根據(jù)傳入的參數(shù)對象是否是Map類型,有兩個不同構(gòu)造ContextMap的方式,而ContextMap作為一個繼承了HashMap的對象,作用就是用于統(tǒng)一參數(shù)的訪問方式:用Map接口方法來訪問數(shù)據(jù)。具體磊說,當(dāng)傳入的參數(shù)對象不是Map類型時,MyBatis會將傳入的POJO對象用MetaObject 對象來封裝,當(dāng)動態(tài)計算sql過程需要獲取數(shù)據(jù)時,用Map 接口的get方法包裝 MetaObject對象的取值過程。

  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;
      //先去map里找
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }
      //如果沒找到,再用ognl表達式去取值
      //如person[0].birthdate.year
      if (parameterMetaObject != null) {
        return parameterMetaObject.getValue(strKey);
      }
      return null;
    }
  }

DynamicContext 的解析到此完成。

解析SQL片段

正如前面所說,一個包含了${}, <if>,<where>等標(biāo)簽的SQL 會被分成很多SQL片段。由SqlNode 的子類進行存儲。
在這里插入圖片描述
如圖所示,

  1. StaticTextSqlNode 用于存儲靜態(tài)文本
  2. TextSqlNode用于存儲帶有${}占位符的文本
  3. ifSqlNode則用于存儲<if>節(jié)點的內(nèi)容
  4. WhereSqlNode 用于增加WHERE 前綴,然后替換掉AND 和OR 等前綴。
  5. MixedSqlNode內(nèi)部維護了一個SqlNode集合,用于存儲各種
    各樣的SqlNode。
    首先我們來看看,MixedSqlNode 的很合SQL節(jié)點。
public class MixedSqlNode implements SqlNode {
  //組合模式,擁有一個SqlNode的List
  private List<SqlNode> contents;

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

  @Override
  public boolean apply(DynamicContext context) {
    //依次調(diào)用list里每個元素的apply
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

如上,從構(gòu)造函數(shù)中可以看出,MixedSqlNode 擁有一個SqlNode的集合。這里利用了組合模式。在此處我重點介紹下TextSqlNode 文本SQL節(jié)點 和IfSqlNode if SQL節(jié)點。

//***TextSqlNode
  public boolean apply(DynamicContext context) {
//    創(chuàng)建${} 占位符解析器
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//   解析${} 占位符,并將解析結(jié)果添加到DynamicContext中
    context.appendSql(parser.parse(text));
    return true;
  }
	
  private GenericTokenParser createParser(TokenHandler handler) {
//    創(chuàng)建占位符解析器,GenericTokenParser 是一個通用解析器,并非只能解析${}
    return new GenericTokenParser("${", "}", handler);
  }

在TextSqlNode 類的內(nèi)部持有了一個綁定記號解析器BindingTokenParser,用于解析標(biāo)記內(nèi)容,并將結(jié)果返回給GenericTokenParser。核心代碼如下:

//***TextSqlNode.BindingTokenParser
    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從用戶傳入的參數(shù)中獲取結(jié)果
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
//     通過正則表達式檢測setValue有效性
      checkInjection(srtValue);
      return srtValue;
    }

而GenericTokenParser 則是一個通用的記號解析器,用戶處理#{}${}參數(shù)。核心代碼如下:

//*GenericTokenParser
  public String parse(String text) {
    StringBuilder builder = new StringBuilder();
    if (text != null && text.length() > 0) {
      char[] src = text.toCharArray();
      int offset = 0;
      int start = text.indexOf(openToken, offset);
      //#{favouriteSection,jdbcType=VARCHAR}
      //這里是循環(huán)解析參數(shù),參考GenericTokenParserTest,比如可以解析${first_name} ${initial} ${last_name} reporting.這樣的字符串,里面有3個 ${}
      while (start > -1) {
    	  //判斷一下 ${ 前面是否是反斜杠,這個邏輯在老版的mybatis中(如3.1.0)是沒有的
        if (start > 0 && src[start - 1] == '\\') {
          // the variable is escaped. remove the backslash.
      	  //新版已經(jīng)沒有調(diào)用substring了,改為調(diào)用如下的offset方式,提高了效率
          //issue #760
          builder.append(src, offset, start - offset - 1).append(openToken);
          offset = start + openToken.length();
        } else {
          int end = text.indexOf(closeToken, start);
          if (end == -1) {
            builder.append(src, offset, src.length - offset);
            offset = src.length;
          } else {
            builder.append(src, offset, start - offset);
            offset = start + openToken.length();
            String content = new String(src, offset, end - offset);
            //得到一對大括號里的字符串后,調(diào)用handler.handleToken,比如替換變量這種功能
            builder.append(handler.handleToken(content));
            offset = end + closeToken.length();
          }
        }
        start = text.indexOf(openToken, offset);
      }
      if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
      }
    }
    return builder.toString();
  }

接著我們來看看IfSqlNode,該Sql節(jié)點主要是判斷<if></if>的條件是否成立,成立的話則調(diào)用其他節(jié)點的apply方法進行解析,并返回true。不成立的話則直接返回false。

  public boolean apply(DynamicContext context) {
    //通過ONGL評估test 表達式的結(jié)果
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
//      若test表達式中的條件成立,則調(diào)用其它節(jié)點的apply方法進行解析。
      contents.apply(context);
      return true;
    }
    return false;
  }

解析#{}占位符

經(jīng)過前面的解析,我們已經(jīng)能夠從DynamicContext 中獲取到完整的SQL語句了。但是這并不意味著解析工作就結(jié)束了。我們還有#{}占位符沒有處理。#{}占位符不同于${}占位符的處理方式。MyBatis 并不會直接將#{}占位符替換成相應(yīng)的參數(shù)值。
#{}的解析過程封裝在SqlSourceBuilder 的parse方法中。解析后的結(jié)果交給StaticSqlSource處理。話不多說,來看看源碼吧。

//*SqlSourceBuilder
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//   創(chuàng)建#{} 占位符處理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //替換#{}中間的部分,如何替換,邏輯在ParameterMappingTokenHandler
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//  解析#{}占位符,并返回解析結(jié)果
    String sql = parser.parse(originalSql);
    //封裝解析結(jié)果到StaticSqlSource中,并返回
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

如上源碼,該解析過程主要有四部,核心步驟就是解析#{}占位符,并返回結(jié)果。GenericTokenParser 類在前面已經(jīng)解析過了,下面我們重點看看SqlSourceBuilder的內(nèi)部類ParameterMappingTokenHandler。該類的核心方法是handleToken方法。該方法的主要作用是將#{}替換成 并返回。然后就是構(gòu)建參數(shù)映射。ParameterMappingTokenHandler 該類同樣實現(xiàn)了TokenHandler 接口,所以GenericTokenParser 類的parse方法可以調(diào)用到。

//參數(shù)映射記號處理器,靜態(tài)內(nèi)部類
  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;
    private MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }

    @Override
    public String handleToken(String content) {
      //獲取context的對應(yīng)的ParameterMapping
      parameterMappings.add(buildParameterMapping(content));
      //如何替換很簡單,永遠是一個問號,但是參數(shù)的信息要記錄在parameterMappings里面供后續(xù)使用
      return "?";
    }

    //構(gòu)建參數(shù)映射
    private ParameterMapping buildParameterMapping(String content) {
        //#{favouriteSection,jdbcType=VARCHAR}
        //先解析參數(shù)映射,就是轉(zhuǎn)化成一個hashmap
      /*
     * parseParameterMapping 內(nèi)部依賴 ParameterExpression 對字符串進行解析,ParameterExpression 的
     */
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      // metaParameters 為 DynamicContext 成員變量 bindings 的元信息對象
      if (metaParameters.hasGetter(property)) {
        /*
     * parameterType 是運行時參數(shù)的類型。如果用戶傳入的是單個參數(shù),比如 Article 對象,此時
     * parameterType 為 Article.class。如果用戶傳入的多個參數(shù),比如 [id = 1, author = "coolblog"],
     * MyBatis 會使用 ParamMap 封裝這些參數(shù),此時 parameterType 為 ParamMap.class。如果
     * parameterType 有相應(yīng)的 TypeHandler,這里則把 parameterType 設(shè)為 propertyType
     */
        propertyType = metaParameters.getGetterType(property);
      } 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) {
        MetaClass metaClass = MetaClass.forClass(parameterType);
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          // 如果 property 為空,或 parameterType 是 Map 類型,則將 propertyType 設(shè)為 Object.class
          propertyType = Object.class;
        }
      } 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,則以用戶的配置為準(zhǔn)。
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);
        }
      }
      //#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

如上,buildParameterMapping方法,主要做了如下三件事

  1. 解析content,
  2. 解析propertyType,對應(yīng)分割線上面的代碼
  3. 構(gòu)建ParameterMapping,對應(yīng)分割下下面的代碼。
    最終的結(jié)果是將 #{xxx} 占位符中的內(nèi)容解析成 Map。
    例如:

    上面占位符中的內(nèi)容最終會被解析成如下的結(jié)果:

           {
               "property": "age",
               "typeHandler": "MyTypeHandler",
               "jdbcType": "NUMERIC",
               "javaType": "int"
           }
    

    BoundSql的創(chuàng)建過程就此結(jié)束了。我們接著往下看。

    創(chuàng)建StatementHandler

    StatementHandler 是非常核心的接口,從代碼分詞的角度來說,StatementHandler是MyBatis源碼的邊界,再往下層就是JDBC層面的接口了。StatementHandler需要和JDBC層面的接口打交道。它要做的事情有很多,在執(zhí)行SQL之前,StatementHandler 需要創(chuàng)建合適的Statement對象。然后填充參數(shù)值到Statement對象中,最后通過Statement 對象執(zhí)行SQL。待SQL執(zhí)行完畢,還需要去處理查詢結(jié)果。
    [外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0cMuXOsM-1582632260584)(./images/1559481523011.png)]

    設(shè)置運行時參數(shù)到SQL中

    JDBC 提供了三種 Statement 接口,分別是 Statement、PreparedStatement 和 CallableStatement。他們的關(guān)系如下:
    [外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-25GvS01Y-1582632260586)(./images/1559481899968.png)]

    上面三個接口的層級分明,其中 Statement 接口提供了執(zhí)行 SQL,獲取執(zhí)行結(jié)果等基本功能。PreparedStatement 在此基礎(chǔ)上,對 IN 類型的參數(shù)提供了支持。使得我們可以使用運行時參數(shù)替換 SQL 中的問號 ? 占位符,而不用手動拼接 SQL。CallableStatement 則是 在 PreparedStatement 基礎(chǔ)上,對 OUT 類型的參數(shù)提供了支持,該種類型的參數(shù)用于保存存儲過程輸出的結(jié)果。

    本節(jié),我將分析 PreparedStatement 的創(chuàng)建,以及設(shè)置運行時參數(shù)到 SQL 中的過程。其他兩種 Statement 的處理過程,大家請自行分析。Statement 的創(chuàng)建入口是在 SimpleExecutor 的 prepareStatement 方法中,下面從這個方法開始進行分析。

    //*SimpleExecutor
      private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
    //   獲取數(shù)據(jù)庫連接
        Connection connection = getConnection(statementLog);
        //創(chuàng)建Statement
        stmt = handler.prepare(connection);
        //為Statement設(shè)置IN參數(shù)
        handler.parameterize(stmt);
        return stmt;
      }
    

    如上,上面代碼的邏輯不復(fù)雜,總共包含三個步驟。如下:

    獲取數(shù)據(jù)庫連接
    創(chuàng)建 Statement
    為 Statement 設(shè)置 IN 參數(shù)
    上面三個步驟看起來并不難實現(xiàn),實際上如果大家愿意寫,也能寫出來。不過 MyBatis 對著三個步驟進行拓展,實現(xiàn)上也相對復(fù)雜一下。以獲取數(shù)據(jù)庫連接為例,MyBatis 并未沒有在 getConnection 方法中直接調(diào)用 JDBC DriverManager 的 getConnection 方法獲取獲取連接,而是通過數(shù)據(jù)源獲取獲取連接。MyBatis 提供了兩種基于 JDBC 接口的數(shù)據(jù)源,分別為 PooledDataSource 和 UnpooledDataSource。創(chuàng)建或獲取數(shù)據(jù)庫連接的操作最終是由這兩個數(shù)據(jù)源執(zhí)行。限于篇幅問題,本節(jié)不打算分析以上兩種數(shù)據(jù)源的源碼,相關(guān)分析會在下一篇文章中展開。

    接下來,我將分析 PreparedStatement 的創(chuàng)建,以及 IN 參數(shù)設(shè)置的過程。按照順序,先來分析 PreparedStatement 的創(chuàng)建過程。如下:

    //*PreparedStatementHandler
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        Statement statement = null;
        try {
            // 創(chuàng)建 Statement
            statement = instantiateStatement(connection);
            // 設(shè)置超時和 FetchSize
            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);
        }
    }
    
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        //調(diào)用Connection.prepareStatement
        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() != null) {
          return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
        } else {
          return connection.prepareStatement(sql);
        }
      }
    

    如上,PreparedStatement 的創(chuàng)建過程沒什么復(fù)雜的地方,就不多說了。下面分析運行時參數(shù)是如何被設(shè)置到 SQL 中的過程。

      public void parameterize(Statement statement) throws SQLException {
        //通過參數(shù)處理器ParameterHandler設(shè)置運行時參數(shù)到PreparedStatement中
        parameterHandler.setParameters((PreparedStatement) statement);
      }
    

    地方

    
     public void setParameters(PreparedStatement ps) throws SQLException {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        /*
        * 從BoundSql中獲取ParameterMapping列表,每個ParameterMapping
        * 與原始SQL中的#{xxx} 占位符一一對應(yīng)
        * */
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          //循環(huán)設(shè)參數(shù)
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
    //       檢測參數(shù)類型,排除掉mode為OUT類型的parameterMapping
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              //如果不是OUT,才設(shè)進去
              Object value;
    //          獲取屬性名
              String propertyName = parameterMapping.getProperty();
    //         檢測BoundSql的additionalParameter是否包含propertyName
              if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                //若有額外的參數(shù), 設(shè)為額外的參數(shù)
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                //若參數(shù)為null,直接設(shè)null
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                //若參數(shù)有相應(yīng)的TypeHandler,直接設(shè)object
                value = parameterObject;
              } else {
                //除此以外,MetaObject.getValue反射取得值設(shè)進去
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }
    //          之上,獲取#{xxx}占位符屬性所對應(yīng)的運行時參數(shù)
    //          -------------------分割線-----------------------
    //      之下,獲取#{xxx}占位符屬性對應(yīng)的TypeHandler,并在最后通過TypeHandler將運行時參數(shù)值設(shè)置到
    //          PreparedStatement中。
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              JdbcType jdbcType = parameterMapping.getJdbcType();
              if (value == null && jdbcType == null) {
                //不同類型的set方法不同,所以委派給子類的setParameter方法
                jdbcType = configuration.getJdbcTypeForNull();
              }
    //        由類型處理器typeHandler向ParameterHandler設(shè)置參數(shù)
              typeHandler.setParameter(ps, i + 1, value, jdbcType);
            }
          }
        }
      }
    

    如上代碼,分割線以上的大段代碼用于獲取 #{xxx} 占位符屬性所對應(yīng)的運行時參數(shù)。分割線以下的代碼則是獲取 #{xxx} 占位符屬性對應(yīng)的 TypeHandler,并在最后通過 TypeHandler 將運行時參數(shù)值設(shè)置到 PreparedStatement 中。關(guān)于 TypeHandler 的用途。




    作者:碼農(nóng)飛哥
    微信公眾號:碼農(nóng)飛哥