mybatis源碼學習(三):MappedStatement的解析過程


我們之前介紹過MappedStatement表示的是XML中的一個SQL。類當中的很多字段都是SQL中對應的屬性。我們先來了解一下這個類的屬性:

public final class MappedStatement {
  
  private String resource;
  private Configuration configuration;
  //sql的ID
  private String id;
  //嘗試影響驅動程序每次批量返回的結果行數和這個設置值相等
  private Integer fetchSize;
  //SQL超時時間
  private Integer timeout;
  //Statement的類型,STATEMENT/PREPARE/CALLABLE
  private StatementType statementType;
  //結果集類型,FORWARD_ONLY/SCROLL_SENSITIVE/SCROLL_INSENSITIVE 
  private ResultSetType resultSetType;
  //表示解析出來的SQL
  private SqlSource sqlSource;
  //緩存
  private Cache cache;
  //已廢棄
  private ParameterMap parameterMap;
  //對應的ResultMap
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  //SQL類型,INSERT/SELECT/DELETE
  private SqlCommandType sqlCommandType;
  //和SELECTKEY標簽有關
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  //數據庫ID,用來區分不同環境
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  //多結果集時
  private String[] resultSets;

  MappedStatement() {
    // constructor disabled
  }

  ...
  }

對一些重要的字段我都增加了備注,方便理解。其中真正表示SQL的字段是SqlSource這個對象。

SqlSource接口很簡單,只有一個getBound方法:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

它有很多實現,需要我們重點關注的是StaticSqlSource,RawSqlSource和DynamicSqlSource。在正式學習他們前,我們先了解一下Mybatis動態SQL和靜態SQL的區別。

動態SQL表示這個SQL節點中含有${}或是其他動態的標簽(比如,if,trim,foreach,choose,bind節點等),需要在運行時根據傳入的條件才能確定SQL,因此對於動態SQL的MappedStatement的解析過程應該是在運行時。

而靜態SQL是不含以上這個節點的SQL,能直接解析得到含有占位符形式的SQL語句,而不需要根據傳入的條件確定SQL,因此可以在加載時就完成解析。所在在執行效率上要高於動態SQL。

而DynamicSqlSource和RawSqlSource就分別對應了動態SQL和靜態SQL,它們都封裝了StaticSqlSource。

我們先從簡單的入手,了解靜態SQL的解析過程。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">


    <select id="selectAllAuthors" resultType="org.apache.ibatis.domain.blog.Author">
        select * from author
    </select>


</mapper>

這是我們要解析的XML文件,mapper節點下只有一個select節點。

public class XmlMapperBuilderTest {

   @Test
  public void shouldSuccessfullyLoadXMLMapperFile() throws Exception {
    Configuration configuration = new Configuration();
    String resource = "org/apache/ibatis/builder/AuthorMapper.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    builder.parse();
    inputStream.close();
  }
}

這是我們測試解析過程的代碼。我們可以看到解析是由XMLMapperBuilder開始的。我們先了解一下它的字段:

public class XMLMapperBuilder extends BaseBuilder {
    //用來解析XML
  private final XPathParser parser;
    //再解析完成后,用解析所得的屬性來幫助創建各個對象
  private final MapperBuilderAssistant builderAssistant;
  //保存SQL節點
  private final Map<String, XNode> sqlFragments;
  //...  
}

它還從父類中繼承了configuration(配置對象),typeAliasRegistry(類型別名注冊器)和typeHandlerRegistry(類型處理器注冊器)。

接下來看一下它的parse方法:

public void parse() {
   //判斷是否已經加載過資源    
    if (!configuration.isResourceLoaded(resource)) {
    //從mapper根節點開始解析
      configurationElement(parser.evalNode("/mapper"));
    //將該資源添加到為已經加載過的緩存中
      configuration.addLoadedResource(resource);
    //將解析的SQL和接口中的方法綁定
      bindMapperForNamespace();
    }
    
    //對一些未完成解析的節點再解析
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
    

主要的解析過程在configurationElement中:

private void configurationElement(XNode context) {
    try {
      //解析mapper的namespace屬性,並設置
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      //解析<cache-ref>節點,它有一個namespace屬性,表示引用該命名空間下的緩存
      cacheRefElement(context.evalNode("cache-ref"));
      //解析<cache>節點,可以設置緩存類型和屬性,或是指定自定義的緩存
      cacheElement(context.evalNode("cache"));
      //已廢棄,不再使用    
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap節點
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析<SQL>節點,SQL節點可以使一些SQL片段被復用     
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析SQL語句(select|insert|update|delete節點) 
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  } 

我們關注SQL語句的解析過程,上述buildStatementFromContext(List<XNode>)方法會增加dateBaseId的參數,然后調用另一個重載方法:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //遍歷XNode節點
    for (XNode context : list) {
      //為每個節點創建XMLStatementBuilder對象,
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
      //解析Node
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        //對不能完全解析的節點添加到incompleteStatement,在parsePendingStatements方法中再解析
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

先看看XMLStatementBuilder對象:

public class XMLStatementBuilder extends BaseBuilder {

  private final MapperBuilderAssistant builderAssistant;
  private final XNode context;
  private final String requiredDatabaseId;
  // ...
}
View Code

含有的字段相對簡單,不再具體解釋。直接看parseStatementNode方法:

 

public void parseStatementNode() {
    //獲取id
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    //驗證databaseId是否匹配
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    //已廢棄
    String parameterMap = context.getStringAttribute("parameterMap");
    //參數類型;將會傳入這條語句的參數類的完全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過 TypeHandler 推斷出具體傳入語句的參數,默認值為 unset。
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //結果類型;外部 resultMap 的命名引用。
    String resultMap = context.getStringAttribute("resultMap");
    //結果類型;表示從這條語句中返回的期望類型的類的完全限定名或別名。注意如果是集合情形,那應該是集合可以包含的類型,而不能是集合本身。不能和resultMap同時使用。
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);


    Class<?> resultTypeClass = resolveClass(resultType);
    //結果集類型;FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個,默認值為 unset (依賴驅動)。
    String resultSetType = context.getStringAttribute("resultSetType");
    //STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    //SQLCommand類型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //flushCache;在執行語句時表示是否刷新緩存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否對該語句進行二級緩存;默認值:對 select 元素為 true。
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //根嵌套結果相關
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //引入SQL片段
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //處理selectKey
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    
    //設置主鍵自增的方式
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //通過buildAssistant將解析得到的參數設置構造成MappedStatement對象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

將解析得到參數通過BuilderAssistant.addMappedStatement方法,解析得到MappedStatement對象。

 

上面已經說過sqlsource表示的一個SQL語句,因此我們關注langDriver.createSqlSource這個方法。看XMLLanguageDriver這個實現。

 @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

可以看到他將創建sqlsource的工作交給了XMLScrpitBuilder(又一個創建者模式的應用)。來看parseScriptNode方法:

public SqlSource parseScriptNode() {
    //解析SQL語句節點,創建MixedSqlNode對象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    //根據是否是動態的語句,創建DynamicSqlSource或是RawSqlSource對象,並返回
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
 }

MixedSqlNode是SqlNode的一個實現,包含了各個子節點,用來遍歷輸出子節點。SqlNode還有很多不同的實現,分別對應不同的節點類型。對應關系如下:

SqlNode實現 對應SQL語句中的類型
TextSqlNode ${}
IfSqlNode If節點
TrimSqlNode/WhereSqlNode/SetSqlNode Trim/Where/Set節點
Foreach節點 foreach標簽
ChooseSqlNode節點 choose/when/otherwhise節點
ValDeclSqlNode節點 bind節點
StaticTextSqlNode 不含上述節點

 

 

 

 

 

 

除了StaticTextSqlNode節點外,其余對應的都是動態語句。

因此我們本文的關注點在StaticTextSqlNode。

讓我們對應文初sql語句的解析來看一下parseDynamicTags方法,為了便於理解,我將在右邊注釋出每一步的結果

protected MixedSqlNode parseDynamicTags(XNode node) {// node是我們要解析的SQL語句: <select resultType="org.apache.ibatis.domain.blog.Author" id="selectAllAuthors">select * from author</select>
    List<SqlNode> contents = new ArrayList<SqlNode>();
    //獲取SQL下面的子節點
    NodeList children = node.getNode().getChildNodes();//這里的children只有一個節點;
    //遍歷子節點,解析成對應的sqlNode類型,並添加到contents中
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));//第一個child節點就是SQL中的文本數據:select * from author
      //如果是文本節點,則先解析成TextSqlNode對象
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        //獲取文本信息
        String data = child.getStringBody("");//data:select * from author
        //創建TextSqlNode對象
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //判斷是否是動態Sql,其過程會調用GenericTokenParser判斷文本中是否含有"${"字符
        if (textSqlNode.isDynamic()) {//如果是動態SQL,則直接使用TextSqlNode類型,並將isDynamic標識置為true
          contents.add(textSqlNode);
          isDynamic = true;
        } else {//不是動態sql,則創建StaticTextSqlNode對象,表示靜態SQL
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他類型的節點,由不同的節點處理器來對應處理成本成不同的SqlNode類型
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    //用contents構建MixedSqlNode對象
    return new MixedSqlNode(contents);
  }

上述過程中,我們主要關注靜態SQL的解析過程,對於動態SQL的解析將在之后介紹。

得到MixedSqlNode后,靜態的SQL會創建出RawSqlSource對象。

看一下RawSqlSource:

  public class RawSqlSource implements SqlSource {
  //內部封裝的sqlSource對象,getBoundSql方法會委托給這個對象
  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
      //創建sqlSourceBuilder
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
       //解析sql,創建StaticSqlSource對象
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

  //獲取sql語句
  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    //這里的rootSqlNode就是之前得到的MixedSqlNode,它會遍歷內部的SqlNode,逐個調用sqlNode的apply方法。StaticTextSqlNode會直接context.appendSql方法
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

}

代碼相對簡單,主要的步驟就是(1)通過SqlNode獲得原始SQL語句;(2)創建SqlSourceBuilder對象,解析SQL語句,並創建StaticSqlSource對象;(3)將getBoundSql方法委托給內部的staticSqlSource對象。

其中比較關鍵的一步是解析原始SQL語句,並創建StaticSqlSource對象。因此我們繼續看SqlSourceBuilder對象。

public class SqlSourceBuilder extends BaseBuilder {

  private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

  public SqlSourceBuilder(Configuration configuration) {
    super(configuration);
  }

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
      //創建TokenHandler,用來將原始Sql中的'#{}' 解析成'?'
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //解析原始sql
    String sql = parser.parse(originalSql);
    //創建出StaticSqlSource對象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

  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) {
      //解析'#{}'中的參數,創建ParameterMapping對象
      parameterMappings.add(buildParameterMapping(content));
      //將'#{}'替換成'?'
      return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        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 || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
      } else {
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          propertyType = Object.class;
        }
      }
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
          // Do Nothing
        } 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));
      }
      return builder.build();
    }

    private Map<String, String> parseParameterMapping(String content) {
      try {
        return new ParameterExpression(content);
      } catch (BuilderException ex) {
        throw ex;
      } catch (Exception ex) {
        throw new BuilderException("Parsing error was found in mapping #{" + content + "}.  Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
      }
    }
  }

}

parse方法主要分為以下幾步:

(1)創建了ParameterMappingTokenHandler對象

(2)將ParameterMappingTokenHandler對象傳入GenericTokenParser的構造函數中,創建GenericTokenParser對象

(3)通過GenericTokenParser對象解析原始SQL,這個過程中會將#{}替換成?,並將#{}中的參數,解析形成ParamterMapping對象

(4)用得到的SQL和ParamterMapping對象創建StaticSqlSource對象。

 

解析完成后回到一開始的XMLMapperBuilder,它會在資源添加到已加載的列表中,並bindMapperForNamespace方法中為創建的MappedStatement添加命名空間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM