MyBatis 源碼分析(六):statement 解析

Mapper 映射文件解析的最后一步是解析所有 statement 元素,即 select搭盾、insertupdate婉支、delete 元素鸯隅,這些元素中可能會包含動態(tài) SQL,即使用 ${} 占位符或 if向挖、choose蝌以、where 等元素動態(tài)組成的 SQL。動態(tài) SQL 功能正是 MyBatis 強(qiáng)大的所在何之,其解析過程也是十分復(fù)雜的跟畅。

解析工具

為了方便 statement 的解析,MyBatis 提供了一些解析工具溶推。

Token 解析

MyBatis 支持使用 ${}#{} 類型的 token 作為動態(tài)參數(shù)徊件,不僅文本中可以使用 tokenxml 元素中的屬性等也可以使用悼潭。

GenericTokenParser

GenericTokenParserMyBatis 提供的通用 token 解析器庇忌,其解析邏輯是根據(jù)指定的 token 前綴和后綴搜索 token舞箍,并使用傳入的 TokenHandler 對文本進(jìn)行處理舰褪。

  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token 搜索 token 前綴
    int start = text.indexOf(openToken);
    if (start == -1) {
      // 沒有 token 前綴,返回原文本
      return text;
    }
    char[] src = text.toCharArray();
    // 當(dāng)前解析偏移量
    int offset = 0;
    // 已解析文本
    final StringBuilder builder = new StringBuilder();
    // 當(dāng)前占位符內(nèi)的表達(dá)式
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // 如果待解析屬性前綴被轉(zhuǎn)義疏橄,則去掉轉(zhuǎn)義字符占拍,加入已解析文本
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        // 更新解析偏移量
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        // 前綴前面的部分加入已解析文本
        builder.append(src, offset, start - offset);
        // 更新解析偏移量
        offset = start + openToken.length();
        // 獲取對應(yīng)的后綴索引
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // 后綴被轉(zhuǎn)義略就,加入已解析文本
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            // 尋找下一個后綴
            end = text.indexOf(closeToken, offset);
          } else {
            // 找到后綴,獲取占位符內(nèi)的表達(dá)式
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // 找不到后綴晃酒,前綴之后的部分全部加入已解析文本
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 能夠找到后綴表牢,追加 token 處理器處理后的文本
          builder.append(handler.handleToken(expression.toString()));
          // 更新解析偏移量
          offset = end + closeToken.length();
        }
      }
      // 尋找下一個前綴,重復(fù)解析表達(dá)式
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      // 將最后的部分加入已解析文本
      builder.append(src, offset, src.length - offset);
    }
    // 返回解析后的文本
    return builder.toString();
  }

由于 GenericTokenParsertoken 前后綴和具體解析邏輯都是可指定的贝次,因此基于 GenericTokenParser 可以實現(xiàn)對不同 token 的定制化解析崔兴。

TokenHandler

TokenHandlertoken 處理器抽象接口。實現(xiàn)此接口可以定義 token 以何種方式被解析蛔翅。

public interface TokenHandler {

  /**
   * 對 token 進(jìn)行解析
   *
   * @param content 待解析 token
   * @return
   */
  String handleToken(String content);
}

PropertyParser

PropertyParsertoken 解析的一種具體實現(xiàn)敲茄,其指定對 ${} 類型 token 進(jìn)行解析,具體解析邏輯由其內(nèi)部類 VariableTokenHandler 實現(xiàn):

  /**
   * 對 ${} 類型 token 進(jìn)行解析
   *
   * @param string
   * @param variables
   * @return
   */
  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

  /**
   * 根據(jù)配置屬性對 ${} token 進(jìn)行解析
   */
  private static class VariableTokenHandler implements TokenHandler {

    /**
     * 預(yù)先設(shè)置的屬性
     */
    private final Properties variables;

    /**
     * 是否運(yùn)行使用默認(rèn)值山析,默認(rèn)為 false
     */
    private final boolean enableDefaultValue;

    /**
     * 默認(rèn)值分隔符號堰燎,即如待解析屬性 ${key:default},key 的默認(rèn)值為 default
     */
    private final String defaultValueSeparator;

    private VariableTokenHandler(Properties variables) {
      this.variables = variables;
      this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
      this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
    }

    private String getPropertyValue(String key, String defaultValue) {
      return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
    }

    @Override
    public String handleToken(String content) {
      if (variables != null) {
        String key = content;
        if (enableDefaultValue) {
          // 如待解析屬性 ${key:default}笋轨,key 的默認(rèn)值為 default
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            // 使用默認(rèn)值
            return variables.getProperty(key, defaultValue);
          }
        }
        if (variables.containsKey(key)) {
          // 不使用默認(rèn)值
          return variables.getProperty(key);
        }
      }
      // 返回原文本
      return "${" + content + "}";
    }
  }

VariableTokenHandler 實現(xiàn)了 TokenHandler 接口秆剪,其構(gòu)造方法允許傳入一組 Properties 用于獲取 token 表達(dá)式的值。如果開啟了使用默認(rèn)值,則表達(dá)式 ${key:default} 會在 key 沒有映射值的時候使用 default 作為默認(rèn)值薇芝。

特殊容器

StrictMap

Configuration 中的 StrictMap 繼承了 HashMap焕阿,相對于 HashMap,其存取鍵值的要求更為嚴(yán)格何什。put 方法不允許添加相同的 key,并獲取最后一個 . 后的部分作為 shortKey等龙,如果 shortKey 也重復(fù)了处渣,其會向容器中添加一個 Ambiguity 對象,當(dāng)使用 get 方法獲取這個 shortKey 對應(yīng)的值時蛛砰,就會拋出異常罐栈。get 方法對于不存在的 key 也會拋出異常。

  public V put(String key, V value) {
    if (containsKey(key)) {
      // 重復(fù) key 異常
      throw new IllegalArgumentException(name + " already contains value for " + key
          + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
    }
    if (key.contains(".")) {
      // 獲取最后一個 . 后的部分作為 shortKey
      final String shortKey = getShortName(key);
      // shortKey 不允許重復(fù)泥畅,否則在獲取時異常
      if (super.get(shortKey) == null) {
        super.put(shortKey, value);
      } else {
        super.put(shortKey, (V) new Ambiguity(shortKey));
      }
    }
    return super.put(key, value);
  }

    public V get(Object key) {
    V value = super.get(key);
    if (value == null) {
      // key 不存在拋異常
      throw new IllegalArgumentException(name + " does not contain value for " + key);
    }
    // 重復(fù)的 key 拋異常
    if (value instanceof Ambiguity) {
      throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
                                         + " (try using the full name including the namespace, or rename one of the entries)");
    }
    return value;
  }

ContextMap

ContextMapDynamicContext 的靜態(tài)內(nèi)部類荠诬,用于保存 sql 上下文中的綁定參數(shù)。

static class ContextMap extends HashMap<String, Object> {
  private static final long serialVersionUID = 2977601501966151582L;

  /**
   * 參數(shù)對象
   */
  private MetaObject parameterMetaObject;

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

  @Override
  public Object get(Object key) {
    // 先根據(jù) key 查找原始容器
    String strKey = (String) key;
    if (super.containsKey(strKey)) {
      return super.get(strKey);
    }

    // 再進(jìn)入?yún)?shù)對象查找
    if (parameterMetaObject != null) {
      // issue #61 do not modify the context when reading
      return parameterMetaObject.getValue(strKey);
    }

    return null;
  }
}

OGNL 工具

OgnlCache

OGNL 工具支持通過字符串表達(dá)式調(diào)用 Java 方法位仁,但是其實現(xiàn)需要對 OGNL 表達(dá)式進(jìn)行編譯柑贞,為了提高性能,MyBatis 提供 OgnlCache 工具類用于對 OGNL 表達(dá)式編譯結(jié)果進(jìn)行緩存聂抢。

  /**
   * 根據(jù) ognl 表達(dá)式和參數(shù)計算值
   *
   * @param expression
   * @param root
   * @return
   */
  public static Object getValue(String expression, Object root) {
    try {
      Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
      return Ognl.getValue(parseExpression(expression), context, root);
    } catch (OgnlException e) {
      throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
    }
  }

  /**
   * 編譯 ognl 表達(dá)式并放入緩存
   *
   * @param expression
   * @return
   * @throws OgnlException
   */
  private static Object parseExpression(String expression) throws OgnlException {
    Object node = expressionCache.get(expression);
    if (node == null) {
      // 編譯 ognl 表達(dá)式
      node = Ognl.parseExpression(expression);
      // 放入緩存
      expressionCache.put(expression, node);
    }
    return node;
  }

ExpressionEvaluator

ExpressionEvaluatorOGNL 表達(dá)式計算工具钧嘶,evaluateBooleanevaluateIterable 方法分別根據(jù)傳入的表達(dá)式和參數(shù)計算出一個 boolean 值或一個可迭代對象。

  /**
   * 計算 ognl 表達(dá)式 true / false
   *
   * @param expression
   * @param parameterObject
   * @return
   */
  public boolean evaluateBoolean(String expression, Object parameterObject) {
    // 根據(jù) ognl 表達(dá)式和參數(shù)計算值
    Object value = OgnlCache.getValue(expression, parameterObject);
    // true / false
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    // 不為 0
    if (value instanceof Number) {
      return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
    }
    // 不為 null
    return value != null;
  }

  /**
   * 計算獲得一個可迭代的對象
   *
   * @param expression
   * @param parameterObject
   * @return
   */
  public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value == null) {
      throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
    }
    if (value instanceof Iterable) {
      // 已實現(xiàn) Iterable 接口
      return (Iterable<?>) value;
    }
    if (value.getClass().isArray()) {
      // 數(shù)組轉(zhuǎn)集合
      // the array may be primitive, so Arrays.asList() may throw
      // a ClassCastException (issue 209).  Do the work manually
      // Curse primitives! :) (JGB)
      int size = Array.getLength(value);
      List<Object> answer = new ArrayList<>();
      for (int i = 0; i < size; i++) {
        Object o = Array.get(value, i);
        answer.add(o);
      }
      return answer;
    }
    if (value instanceof Map) {
      // Map 獲取 entry
      return ((Map) value).entrySet();
    }
    throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
  }

解析邏輯

MyBastis 中調(diào)用 XMLStatementBuilder#parseStatementNode 方法解析單個 statement 元素琳疏。此方法中除了逐個獲取元素屬性有决,還對 include 元素闸拿、selectKey 元素進(jìn)行解析,創(chuàng)建了 sql 生成對象 SqlSource书幕,并將 statement 的全部信息聚合到 MappedStatement 對象中新荤。

  public void parseStatementNode() {
    // 獲取 id
    String id = context.getStringAttribute("id");
    // 自定義數(shù)據(jù)庫廠商信息
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      // 不符合當(dāng)前數(shù)據(jù)源對應(yīng)的數(shù)據(jù)廠商信息的語句不加載
      return;
    }

    // 獲取元素名
    String nodeName = context.getNode().getNodeName();
    // 元素名轉(zhuǎn)為對應(yīng)的 SqlCommandType 枚舉
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 是否為查詢
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 獲取 flushCache 屬性,查詢默認(rèn)為 false台汇,其它默認(rèn)為 true
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // 獲取 useCache 屬性苛骨,查詢默認(rèn)為 true,其它默認(rèn)為 false
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    // 獲取 resultOrdered 屬性苟呐,默認(rèn)為 false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    // 解析 include 屬性
    includeParser.applyIncludes(context.getNode());

    // 參數(shù)類型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    // 獲取 Mapper 語法類型
    String lang = context.getStringAttribute("lang");
    // 默認(rèn)使用 XMLLanguageDriver
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // 解析 selectKey 元素
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 獲取 KeyGenerator
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      // 獲取解析完成的 KeyGenerator 對象
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      // 如果開啟了 useGeneratedKeys 屬性智袭,并且為插入類型的 sql 語句、配置了 keyProperty 屬性掠抬,則可以批量自動設(shè)置屬性
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 生成有效 sql 語句和參數(shù)綁定對象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // sql 類型
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 分批獲取數(shù)據(jù)的數(shù)量
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // 執(zhí)行超時時間
    Integer timeout = context.getIntAttribute("timeout");
    // 參數(shù)映射
    String parameterMap = context.getStringAttribute("parameterMap");
    // 返回值類型
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // 返回值映射 map
    String resultMap = context.getStringAttribute("resultMap");
    // 結(jié)果集類型
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    // 插入吼野、更新生成鍵值的字段
    String keyProperty = context.getStringAttribute("keyProperty");
    // 插入、更新生成鍵值的列
    String keyColumn = context.getStringAttribute("keyColumn");
    // 指定多結(jié)果集名稱
    String resultSets = context.getStringAttribute("resultSets");

    // 新增 MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

語法驅(qū)動

LanguageDriverstatement 創(chuàng)建語法驅(qū)動两波,默認(rèn)實現(xiàn)為 XMLLanguageDriver瞳步,其提供 createSqlSource 方法用于使用 XMLScriptBuilder 創(chuàng)建 sql 生成對象。

遞歸解析 include

include 元素是 statement 元素的子元素腰奋,通過 refid 屬性可以指向在別處定義的 sql fragments单起。

  public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    // 拷貝全局配置中設(shè)置的額外配置屬性
    Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
    applyIncludes(source, variablesContext, false);
  }

  /**
   * 遞歸解析 statement 元素中的 include 元素
   *
   * Recursively apply includes through all SQL fragments.
   * @param source Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if (source.getNodeName().equals("include")) {
      // include 元素,從全局配置中找對應(yīng)的 sql 節(jié)點(diǎn)并 clone
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      // 讀取 include 子元素中的 property 元素劣坊,獲取全部屬性
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        // include 指向的 sql clone 節(jié)點(diǎn)嘀倒,逐個對屬性進(jìn)行解析
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      // statement 元素中可能包含 include 子元素
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && source.getNodeType() == Node.TEXT_NODE
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      // 替換元素值,如果使用了 ${} 占位符局冰,會對 token 進(jìn)行解析
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

  /**
   * 從全局配置中找對應(yīng)的 sql fragment
   *
   * @param refid
   * @param variables
   * @return
   */
  private Node findSqlFragment(String refid, Properties variables) {
    // 解析 refid
    refid = PropertyParser.parse(refid, variables);
    // namespace.refid
    refid = builderAssistant.applyCurrentNamespace(refid, true);
    try {
      // 從全局配置中找對應(yīng)的 sql fragment
      XNode nodeToInclude = configuration.getSqlFragments().get(refid);
      return nodeToInclude.getNode().cloneNode(true);
    } catch (IllegalArgumentException e) {
      // sql fragments 定義在全局配置中的 StrictMap 中测蘑,獲取不到會拋出異常
      throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
    }
  }

  private String getStringAttribute(Node node, String name) {
    return node.getAttributes().getNamedItem(name).getNodeValue();
  }

  /**
   * Read placeholders and their values from include node definition.
   *
   * 讀取 include 子元素中的 property 元素
   * @param node Include node instance
   * @param inheritedVariablesContext Current context used for replace variables in new variables values
   * @return variables context from include instance (no inherited values)
   */
  private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
    Map<String, String> declaredProperties = null;
    // 解析 include 元素中的 property 子元素
    NodeList children = node.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node n = children.item(i);
      if (n.getNodeType() == Node.ELEMENT_NODE) {
        // include 運(yùn)行包含 property 元素
        String name = getStringAttribute(n, "name");
        // Replace variables inside
        String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
        if (declaredProperties == null) {
          declaredProperties = new HashMap<>();
        }
        if (declaredProperties.put(name, value) != null) {
          // 不允許添加同名屬性
          throw new BuilderException("Variable " + name + " defined twice in the same include definition");
        }
      }
    }
    if (declaredProperties == null) {
      return inheritedVariablesContext;
    } else {
      // 聚合屬性配置
      Properties newProperties = new Properties();
      newProperties.putAll(inheritedVariablesContext);
      newProperties.putAll(declaredProperties);
      return newProperties;
    }
  }

在開始解析前,從全局配置中獲取全部的屬性配置康二,如果 include 元素中有 property 元素碳胳,解析并獲取鍵值,放入 variablesContext 中沫勿,在后續(xù)處理中針對可能出現(xiàn)的 ${} 類型 token 使用 PropertyParser 進(jìn)行解析挨约。

因為解析 statement 元素前已經(jīng)加載過 sql 元素,因此會根據(jù) include 元素的 refid 屬性查找對應(yīng)的 sql fragments产雹,如果全局配置中無法找到就會拋出異常诫惭;如果能夠找到則克隆 sql 元素并插入到當(dāng)前 xml 文檔中。

解析 selectKey

selectKey 用于指定 sqlinsertupdate 語句執(zhí)行前或執(zhí)行后生成或獲取列值蔓挖,在 MyBatisselectKey 也被當(dāng)做 statement 語句進(jìn)行解析并設(shè)置到全局配置中夕土。單個 selectKey 元素會以SelectKeyGenerator 對象的形式進(jìn)行保存用于后續(xù)調(diào)用。

  private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    // 返回值類型
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 對應(yīng)字段名
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    // 對應(yīng)列名
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 是否在父sql執(zhí)行前執(zhí)行
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    //defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    // 創(chuàng)建 sql 生成對象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;

    // 將 KeyGenerator 生成 sql 作為 MappedStatement 加入全局對象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    // 包裝為 SelectKeyGenerator 對象
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  }

selectKey 解析完成后时甚,按指定的 namespace 規(guī)則從全局配置中獲取 SelectKeyGenerator 對象隘弊,等待創(chuàng)建 MappedStatement 對象。如果未指定 selectKey 元素荒适,但是全局配置中開啟了 useGeneratedKeys梨熙,并且指定 insert 元素的 useGeneratedKeys 屬性為 true,則 MyBatis 會指定 Jdbc3KeyGenerator 作為 useGeneratedKeys 的默認(rèn)實現(xiàn)刀诬。

創(chuàng)建 sql 生成對象

SqlSource

SqlSourcesql 生成抽象接口咽扇,其提供 getBoundSql 方法用于根據(jù)參數(shù)生成有效 sql 語句和參數(shù)綁定對象 BoundSql。在生成 statement 元素的解析結(jié)果 MappedStatement 對象前陕壹,需要先創(chuàng)建 sql 生成對象质欲,即 SqlSource 對象。

public interface SqlSource {

  /**
   * 根據(jù)參數(shù)生成有效 sql 語句和參數(shù)綁定對象
   *
   * @param parameterObject
   * @return
   */
  BoundSql getBoundSql(Object parameterObject);

}

SqlNode

SqlNodesql 節(jié)點(diǎn)抽象接口糠馆。sql 節(jié)點(diǎn)指的是 statement 中的組成部分嘶伟,如果簡單文本、if 元素又碌、where 元素等九昧。SqlNode 提供 apply 方法用于判斷當(dāng)前 sql 節(jié)點(diǎn)是否可以加入到生效的 sql 語句中。

public interface SqlNode {

  /**
   * 根據(jù)條件判斷當(dāng)前 sql 節(jié)點(diǎn)是否可以加入到生效的 sql 語句中
   *
   * @param context
   * @return
   */
  boolean apply(DynamicContext context);
}
SqlNode 體系.png

DynamicContext

DynamicContext 是動態(tài) sql 上下文毕匀,用于保存綁定參數(shù)和生效 sql 節(jié)點(diǎn)铸鹰。DynamicContext 使用 ContextMap 作為參數(shù)綁定容器。由于動態(tài) sql 是根據(jù)參數(shù)條件組合生成 sql皂岔,DynamicContext 還提供了對 sqlBuilder 修改和訪問方法蹋笼,用于添加有效 sql 節(jié)點(diǎn)和生成 sql 文本。

  /**
   * 生效的 sql 部分躁垛,以空格相連
   */
  private final StringJoiner sqlBuilder = new StringJoiner(" ");

  public void appendSql(String sql) {
    sqlBuilder.add(sql);
  }

  public String getSql() {
    return sqlBuilder.toString().trim();
  }

節(jié)點(diǎn)解析

statement 元素轉(zhuǎn)為 sql 生成對象依賴于 LanguageDrivercreateSqlSource 方法剖毯,此方法中創(chuàng)建 XMLScriptBuilder 對象,并調(diào)用 parseScriptNode 方法對 sql 組成節(jié)點(diǎn)逐個解析并進(jìn)行組合教馆。

  public SqlSource parseScriptNode() {
    // 遞歸解析各 sql 節(jié)點(diǎn)
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      // 動態(tài) sql
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 原始文本 sql
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

  /**
   * 處理 statement 各 SQL 組成部分速兔,并進(jìn)行組合
   */
  protected MixedSqlNode parseDynamicTags(XNode node) {
    // SQL 各組成部分
    List<SqlNode> contents = new ArrayList<>();
    // 遍歷子元素
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        // 解析 sql 文本
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          // 判斷是否為動態(tài) sql,包含 ${} 占位符即為動態(tài) sql
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 靜態(tài) sql 元素
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        // 如果是子元素
        String nodeName = child.getNode().getNodeName();
        // 獲取支持的子元素語法處理器
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        // 根據(jù)子元素標(biāo)簽類型使用對應(yīng)的處理器處理子元素
        handler.handleNode(child, contents);
        // 包含標(biāo)簽元素活玲,認(rèn)定為動態(tài) SQL
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

parseDynamicTags 方法會對 sql 各組成部分進(jìn)行分解涣狗,如果 statement 元素包含 ${} 類型 token 或含有標(biāo)簽子元素,則認(rèn)為當(dāng)前 statement 是動態(tài) sql舒憾,隨后 isDynamic 屬性會被設(shè)置為 true镀钓。對于文本節(jié)點(diǎn),如 sql 純文本和僅含 ${} 類型 token 的文本镀迂,會被包裝為 StaticTextSqlNodeTextSqlNode 加入到 sql 節(jié)點(diǎn)容器中丁溅,而其它元素類型的 sql 節(jié)點(diǎn)會經(jīng)過 NodeHandlerhandleNode 方法處理過之后才能加入到節(jié)點(diǎn)容器中。nodeHandlerMap 定義了不同動態(tài) sql 元素節(jié)點(diǎn)與 NodeHandler 的關(guān)系:

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }
MixedSqlNode

MixedSqlNode 中定義了一個 SqlNode 集合探遵,用于保存 statement 中包含的全部 sql 節(jié)點(diǎn)窟赏。其生成有效 sql 的邏輯為逐個判斷節(jié)點(diǎn)是否有效妓柜。

  /**
   * 組合 SQL 各組成部分
   *
   * @author Clinton Begin
   */
  public class MixedSqlNode implements SqlNode {

    /**
     * SQL 各組裝成部分
     */
    private final List<SqlNode> contents;

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

    @Override
    public boolean apply(DynamicContext context) {
      // 逐個判斷各個 sql 節(jié)點(diǎn)是否能生效
      contents.forEach(node -> node.apply(context));
      return true;
    }
  }
StaticTextSqlNode

StaticTextSqlNode 中僅包含靜態(tài) sql 文本,在組裝時會直接追加到 sql 上下文的有效 sql 中:

  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }
TextSqlNode

TextSqlNode 中的 sql 文本包含 ${} 類型 token涯穷,使用 GenericTokenParser 搜索到 token 后會使用 BindingTokenParsertoken 進(jìn)行解析棍掐,解析后的文本會被追加到生效 sql 中。

  @Override
  public boolean apply(DynamicContext context) {
    // 搜索 ${} 類型 token 節(jié)點(diǎn)
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 解析 token 并追加解析后的文本到生效 sql 中
    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) {
      // 獲取綁定參數(shù)
      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);
      }
      // 計算 ognl 表達(dá)式的值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }
  }
IfSqlNode

if 標(biāo)簽用于在 test 條件生效時才追加標(biāo)簽內(nèi)的文本拷况。

  ...
  <if test="userId > 0">
    AND user_id = #{userId}
  </if>

IfSqlNode 保存了 if 元素下的節(jié)點(diǎn)內(nèi)容和 test 表達(dá)式作煌,在生成有效 sql 時會根據(jù) OGNL 工具計算 test 表達(dá)式是否生效。

  @Override
  public boolean apply(DynamicContext context) {
    // 根據(jù) test 表達(dá)式判斷當(dāng)前節(jié)點(diǎn)是否生效
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
TrimSqlNode

trim 標(biāo)簽用于解決動態(tài) sql 中由于條件不同不能拼接正確語法的問題赚瘦。

  SELECT * FROM test
  <trim prefix="WHERE" prefixOverrides="AND|OR">
    <if test="a > 0">
      a = #{a}
    </if>
    <if test="b > 0">
      OR b = #粟誓
    </if>
    <if test="c > 0">
      AND c = #{c}
    </if>
  </trim>

如果沒有 trim 標(biāo)簽,這個 statement 的有效 sql 最終可能會是這樣的:

SELECT * FROM test OR b = #起意

但是加上 trim 標(biāo)簽鹰服,生成的 sql 語法是正確的:

SELECT * FROM test WHERE b = #

prefix 屬性用于指定 trim 節(jié)點(diǎn)生成的 sql 語句的前綴揽咕,prefixOverrides 則會指定生成的 sql 語句的前綴需要去除的部分获诈,多個需要去除的前綴可以使用 | 隔開。suffixsuffixOverrides 的功能類似心褐,但是作用于后綴舔涎。

TrimSqlNode 首先調(diào)用 parseOverridesprefixOverridessuffixOverrides 進(jìn)行解析,通過 | 分隔逗爹,分別加入字符串集合亡嫌。

  private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
      // 解析 token,按 | 分隔
      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
      final List<String> list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
        // 保存為字符串集合
        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
    }
    return Collections.emptyList();
  }

在調(diào)用包含的 SqlNodeapply 方法后還會調(diào)用 FilteredDynamicContextapplyAll 方法處理前綴和后綴掘而。

  @Override
  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    // 加上前綴和和后綴挟冠,并去除多余字段
    filteredDynamicContext.applyAll();
    return result;
  }

對于已經(jīng)生成的 sql 文本,分別根據(jù)規(guī)則加上和去除指定前綴和后綴袍睡。

    public void applyAll() {
    sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
    String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
    if (trimmedUppercaseSql.length() > 0) {
      // 加上前綴和和后綴知染,并去除多余字段
      applyPrefix(sqlBuffer, trimmedUppercaseSql);
      applySuffix(sqlBuffer, trimmedUppercaseSql);
    }
    delegate.appendSql(sqlBuffer.toString());
  }

    private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
    if (!prefixApplied) {
      prefixApplied = true;
      if (prefixesToOverride != null) {
        // 文本最前去除多余字段
        for (String toRemove : prefixesToOverride) {
          if (trimmedUppercaseSql.startsWith(toRemove)) {
            sql.delete(0, toRemove.trim().length());
            break;
          }
        }
      }
      // 在文本最前插入前綴和空格
      if (prefix != null) {
        sql.insert(0, " ");
        sql.insert(0, prefix);
      }
    }
  }

  private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
    if (!suffixApplied) {
      suffixApplied = true;
      if (suffixesToOverride != null) {
        // 文本最后去除多余字段
        for (String toRemove : suffixesToOverride) {
          if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
            int start = sql.length() - toRemove.trim().length();
            int end = sql.length();
            sql.delete(start, end);
            break;
          }
        }
      }
      // 文本最后插入空格和后綴
      if (suffix != null) {
        sql.append(" ");
        sql.append(suffix);
      }
    }
  }
WhereSqlNode

where 元素與 trim 元素的功能類似,區(qū)別在于 where 元素不提供屬性配置可以處理的前綴和后綴斑胜。

  ...
  <where>
    ...
  </where>

WhereSqlNode 繼承了 TrimSqlNode控淡,并指定了需要添加和刪除的前綴。

public class WhereSqlNode extends TrimSqlNode {

  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    // 默認(rèn)添加 WHERE 前綴止潘,去除 AND掺炭、OR 等前綴
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

因此,生成的 sql 語句會自動在最前加上 WHERE凭戴,并去除前綴中包含的 AND涧狮、OR 等字符串。

SetSqlNode

set 標(biāo)簽用于 update 語句中。

    UPDATE test
    <set>
        <if test="a > 0">
      a = #{a},
    </if>
        <if test="b > 0">
      b = #者冤
    </if>
    </set>

SetSqlNode 同樣繼承自 TrimSqlNode肤视,并指定默認(rèn)添加 SET 前綴,去除 , 前綴和后綴涉枫。

public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    // 默認(rèn)添加 SET 前綴邢滑,去除 , 前綴和后綴
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}
ForEachSqlNode

foreach 元素用于指定對集合循環(huán)添加 sql 語句。

...
<foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
  AND itm = #{item} AND idx #{index}
</foreach>

ForEachSqlNode 解析生成有效 sql 的邏輯如下拜银,除了計算 collection 表達(dá)式的值殊鞭、添加前綴遭垛、后綴外尼桶,還將參數(shù)與索引進(jìn)行了綁定。

  @Override
  public boolean apply(DynamicContext context) {
    // 獲取綁定參數(shù)
    Map<String, Object> bindings = context.getBindings();
    // 計算 ognl 表達(dá)式獲取可迭代對象
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    // 添加動態(tài)語句前綴
    applyOpen(context);
    // 迭代索引
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      // 首個元素
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
        // entry 集合項索引為 key锯仪,集合項為 value
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        // 綁定集合項索引關(guān)系
        applyIndex(context, i, uniqueNumber);
        // 綁定集合項關(guān)系
        applyItem(context, o, uniqueNumber);
      }
      // 對解析的表達(dá)式進(jìn)行替換泵督,如 idx = #{index} AND itm = #{item} 替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    // 添加動態(tài)語句后綴
    applyClose(context);
    // 移除原始的表達(dá)式
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

  /**
   * 綁定集合項索引關(guān)系
   *
   * @param context
   * @param o
   * @param i
   */
  private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
      context.bind(index, o);
      context.bind(itemizeItem(index, i), o);
    }
  }

  /**
   * 綁定集合項關(guān)系
   *
   * @param context
   * @param o
   * @param i
   */
  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      context.bind(item, o);
      context.bind(itemizeItem(item, i), o);
    }
  }

對于循環(huán)中的 #{} 類型 tokenForEachSqlNode 在內(nèi)部類 FilteredDynamicContext 中定義了解析規(guī)則:

  @Override
  public void appendSql(String sql) {
    GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
      // 對解析的表達(dá)式進(jìn)行替換庶喜,如 idx = #{index} AND itm = #{item} 替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}
      String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
      if (itemIndex != null && newContent.equals(content)) {
        newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
      }
      return "#{" + newContent + "}";
    });

    delegate.appendSql(parser.parse(sql));
  }

類似 idx = #{index} AND itm = #{item} 會被替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}小腊,而 ForEachSqlNode 也做了參數(shù)與索引的綁定,因此在替換時可以快速綁定參數(shù)久窟。

ChooseSqlNode

choose 元素用于生成帶默認(rèn) sql 文本的語句秩冈,當(dāng) when 元素中的條件都不生效,就可以使用 otherwise 元素的默認(rèn)文本斥扛。

  ...
  <choose>
        <when test="a > 0">
        AND a = #{a}
    </when>
    <otherwise>
        AND b = #入问
    </otherwise>
  </choose>

ChooseSqlNode 是由 choose 節(jié)點(diǎn)和 otherwise 節(jié)點(diǎn)組合而成的,在生成有效 sql 于語句時會逐個計算 when 節(jié)點(diǎn)的 test 表達(dá)式稀颁,如果返回 true 則生效當(dāng)前 when 語句中的 sql芬失。如果均不生效則使用 otherwise 語句對應(yīng)的默認(rèn) sql 文本。

@Override
public boolean apply(DynamicContext context) {
  // when 節(jié)點(diǎn)根據(jù) test 表達(dá)式判斷是否生效
  for (SqlNode sqlNode : ifSqlNodes) {
    if (sqlNode.apply(context)) {
      return true;
    }
  }

  // when 節(jié)點(diǎn)如果都未生效匾灶,且存在 otherwise 節(jié)點(diǎn)棱烂,則使用 otherwise 節(jié)點(diǎn)
  if (defaultSqlNode != null) {
    defaultSqlNode.apply(context);
    return true;
  }
  return false;
}
VarDeclSqlNode

bind 元素用于綁定一個 OGNL 表達(dá)式到一個動態(tài) sql 變量中。

<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />

VarDeclSqlNode 會計算表達(dá)式的值并將參數(shù)名和值綁定到參數(shù)容器中阶女。

@Override
public boolean apply(DynamicContext context) {
  // 解析 ognl 表達(dá)式
  final Object value = OgnlCache.getValue(expression, context.getBindings());
  // 綁定參數(shù)
  context.bind(name, value);
  return true;
}

創(chuàng)建解析對象與生成可執(zhí)行 sql

statemen 解析完畢后會創(chuàng)建 MappedStatement 對象颊糜,statement 的相關(guān)屬性以及生成的 sql 創(chuàng)建對象都會被保存到該對象中。MappedStatement 還提供了 getBoundSql 方法用于獲取可執(zhí)行 sql 和參數(shù)綁定對象秃踩,即 BoundSql 對象芭析。

public BoundSql getBoundSql(Object parameterObject) {
  // 生成可執(zhí)行 sql 和參數(shù)綁定對象
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  // 獲取參數(shù)映射
  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)
  // 檢查是否有嵌套的 resultMap
  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;
}

BoundSql 對象由 DynamicSqlSourcegetBoundSql 方法生成,在驗證各個 sql 節(jié)點(diǎn)吞瞪,生成了有效 sql 后會繼續(xù)調(diào)用 SqlSourceBuildersql 解析為 StaticSqlSource馁启,即可執(zhí)行 sql

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 驗證各 sql 節(jié)點(diǎn),生成有效 sql
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 將生成的 sql 文本解析為 StaticSqlSource
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

此時的 sql 文本中仍包含 #{} 類型 token惯疙,需要通過 ParameterMappingTokenHandler 進(jìn)行解析翠勉。

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 創(chuàng)建 #{} 類型 token 搜索對象
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 解析 token
    String sql = parser.parse(originalSql);
    // 創(chuàng)建靜態(tài) sql 生成對象,并綁定參數(shù)
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

token 的具體解析邏輯為根據(jù)表達(dá)式的參數(shù)名生成對應(yīng)的參數(shù)映射對象霉颠,并將表達(dá)式轉(zhuǎn)為預(yù)編譯 sql 的占位符 ?对碌。

    @Override
  public String handleToken(String content) {
    // 創(chuàng)建參數(shù)映射對象
    parameterMappings.add(buildParameterMapping(content));
    // 將表達(dá)式轉(zhuǎn)為預(yù)編譯 sql 占位符
    return "?";
  }

最終解析完成的 sql 與參數(shù)映射關(guān)系集合包裝為 StaticSqlSource 對象,該對象在隨后的邏輯中通過構(gòu)造方法創(chuàng)建了 BoundSql 對象蒿偎。

接口解析

除了使用 xml 方式配置 statement朽们,MyBatis 同樣支持使用 Java 注解配置。但是相對于 xml 的映射方式诉位,將動態(tài) sql 寫在 Java 代碼中是不合適的骑脱。如果在配置文件中指定了需要注冊 Mapper 接口的類或包,MyBatis 會掃描相關(guān)類進(jìn)行注冊苍糠;在 Mapper 文件解析完成后也會嘗試加載 namespace 的同名類叁丧,如果存在,則注冊為 Mapper 接口岳瞭。

無論是綁定還是直接注冊 Mapper 接口拥娄,都是調(diào)用 MapperAnnotationBuilder#parse 方法來解析的。此方法中的解析方式與上述 xml 解析方式大致相同瞳筏,區(qū)別只在于相關(guān)配置參數(shù)是從注解中獲取而不是從 xml 元素屬性中獲取稚瘾。

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        // 不允許相同接口重復(fù)注冊
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

小結(jié)

statement 解析的最終目的是為每個 statement 創(chuàng)建一個 MappedStatement 對象保存相關(guān)定義,在 sql 執(zhí)行時根據(jù)傳入?yún)?shù)動態(tài)獲取可執(zhí)行 sql 和參數(shù)綁定對象姚炕。

  • org.apache.ibatis.builder.xml.XMLStatementBuilder:解析 Mapper 文件中的 select|insert|update|delete 元素摊欠。
  • org.apache.ibatis.parsing.GenericTokenParser.GenericTokenParser:搜索指定格式 token 并進(jìn)行解析。
  • org.apache.ibatis.parsing.TokenHandlertoken 處理器抽象接口钻心。定義 token 以何種方式被解析凄硼。
  • org.apache.ibatis.parsing.PropertyParser${} 類型 token 解析器。
  • org.apache.ibatis.session.Configuration.StrictMap:封裝 HashMap捷沸,對鍵值存取有嚴(yán)格要求摊沉。
  • org.apache.ibatis.builder.xml.XMLIncludeTransformerinclude 元素解析器。
  • org.apache.ibatis.mapping.SqlSourcesql 生成抽象接口痒给。根據(jù)傳入?yún)?shù)生成有效 sql 語句和參數(shù)綁定對象说墨。
  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:解析 statement 各個 sql 節(jié)點(diǎn)并進(jìn)行組合。
  • org.apache.ibatis.scripting.xmltags.SqlNodesql 節(jié)點(diǎn)抽象接口苍柏。用于判斷當(dāng)前 sql 節(jié)點(diǎn)是否可以加入到生效的 sql 語句中尼斧。
  • org.apache.ibatis.scripting.xmltags.DynamicContext:動態(tài) sql 上下文。用于保存綁定參數(shù)和生效 sql 節(jié)點(diǎn)试吁。
  • org.apache.ibatis.scripting.xmltags.OgnlCacheognl 緩存工具棺棵,緩存表達(dá)式編譯結(jié)果楼咳。
  • org.apache.ibatis.scripting.xmltags.ExpressionEvaluatorognl 表達(dá)式計算工具。
  • org.apache.ibatis.scripting.xmltags.MixedSqlNodesql 節(jié)點(diǎn)組合對象烛恤。
  • org.apache.ibatis.scripting.xmltags.StaticTextSqlNode:靜態(tài) sql 節(jié)點(diǎn)對象母怜。
  • org.apache.ibatis.scripting.xmltags.TextSqlNode${} 類型 sql 節(jié)點(diǎn)對象。
  • org.apache.ibatis.scripting.xmltags.IfSqlNodeif 元素 sql 節(jié)點(diǎn)對象缚柏。
  • org.apache.ibatis.scripting.xmltags.TrimSqlNodetrim 元素 sql 節(jié)點(diǎn)對象苹熏。
  • org.apache.ibatis.scripting.xmltags.WhereSqlNodewhere 元素 sql 節(jié)點(diǎn)對象。
  • org.apache.ibatis.scripting.xmltags.SetSqlNodeset 元素 sql 節(jié)點(diǎn)對象币喧。
  • org.apache.ibatis.scripting.xmltags.ForEachSqlNodeforeach 元素 sql 節(jié)點(diǎn)對象轨域。
  • org.apache.ibatis.scripting.xmltags.ChooseSqlNodechoose 元素 sql 節(jié)點(diǎn)對象。
  • org.apache.ibatis.scripting.xmltags.VarDeclSqlNodebind 元素 sql 節(jié)點(diǎn)對象杀餐。
  • org.apache.ibatis.mapping.MappedStatementstatement 解析對象干发。
  • org.apache.ibatis.mapping.BoundSql:可執(zhí)行 sql 和參數(shù)綁定對象。
  • org.apache.ibatis.scripting.xmltags.DynamicSqlSource:根據(jù)參數(shù)動態(tài)生成有效 sql 和綁定參數(shù)怜浅。
  • org.apache.ibatis.builder.SqlSourceBuilder:解析 #{} 類型 token 并綁定參數(shù)對象铐然。

注釋源碼

注釋源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔬崩,一起剝皮案震驚了整個濱河市恶座,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沥阳,老刑警劉巖跨琳,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桐罕,居然都是意外死亡脉让,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門功炮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來溅潜,“玉大人,你說我怎么就攤上這事薪伏」隼剑” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵嫁怀,是天一觀的道長设捐。 經(jīng)常有香客問我,道長塘淑,這世上最難降的妖魔是什么萝招? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮存捺,結(jié)果婚禮上槐沼,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好岗钩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凹嘲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪趋艘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天瓷胧,我揣著相機(jī)與錄音棚愤,去河邊找鬼。 笑死宛畦,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的反肋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼踏施,長吁一口氣:“原來是場噩夢啊……” “哼石蔗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起畅形,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤养距,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后日熬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棍厌,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年碍遍,在試婚紗的時候發(fā)現(xiàn)自己被綠了定铜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡怕敬,死狀恐怖揣炕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情东跪,我是刑警寧澤畸陡,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布鹰溜,位于F島的核電站,受9級特大地震影響丁恭,放射性物質(zhì)發(fā)生泄漏曹动。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一牲览、第九天 我趴在偏房一處隱蔽的房頂上張望墓陈。 院中可真熱鬧,春花似錦贡必、人聲如沸仔拟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至刚盈,卻和暖如春藕漱,著一層夾襖步出監(jiān)牢的瞬間肋联,已是汗流浹背刁俭。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工牍戚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留如孝,地道東北人第晰。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像品抽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子圆恤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容