mybatis源碼閱讀(三):mybatis初始化(下)mapper解析

MyBatis 的真正強大在於它的映射語句,也是它的魔力所在。因爲它的異常強大,映射器的 XML 文件就顯得相對簡單。若是拿它跟具備相同功能的 JDBC 代碼進行對比,你會當即發現省掉了將近 95% 的代碼。MyBatis 就是針對 SQL 構建的,而且比普通的方法作的更好。html

SQL 映射文件有不多的幾個頂級元素(按照它們應該被定義的順序):java

  • cache – 給定命名空間的緩存配置。
  • cache-ref – 其餘命名空間緩存配置的引用。
  • resultMap – 是最複雜也是最強大的元素,用來描述如何從數據庫結果集中來加載對象。
  • parameterMap – 已廢棄!老式風格的參數映射。內聯參數是首選,這個元素可能在未來被移除,這裏不會記錄。
  • sql – 可被其餘語句引用的可重用語句塊。
  • insert – 映射插入語句
  • update – 映射更新語句
  • delete – 映射刪除語句
  • select – 映射查詢語句

對每一個標籤的屬性以及做用,這裏不作解釋, 能夠參考官方文檔:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.htmlnode

上一篇文章介紹了mybatis配置文件解析mappers節點的源碼中有以下語句,從這裏獲得mapper映射文件時經過XMLMapperBuilder解析的。算法

1、XMLMapperBuilder

//mapper映射文件都是經過XMLMapperBuilder解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
//解析mapper文件
public void parse() {
  // 判斷是否已經加載過改映射文件
  if (!configuration.isResourceLoaded(resource)) {
    // 處理mapper節點
    configurationElement(parser.evalNode("/mapper"));
    // 將resource添加到configuration的loadedResources集合中保存 它是HashSet<String>
    configuration.addLoadedResource(resource);
    //註冊mapper接口
    bindMapperForNamespace();
  }
  // 處理解析失敗的resultMap節點
  parsePendingResultMaps();
  // 處理解析失敗的cache-ref節點
  parsePendingCacheRefs();
  // 處理解析失敗的sql節點
  parsePendingStatements();
}
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // 記錄當前命名空間
    builderAssistant.setCurrentNamespace(namespace);
    // 解析cache-ref節點
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析cache節點
    cacheElement(context.evalNode("cache"));
    // 解析parameterMap節點,這個已經被廢棄,不推薦使用
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析resultMap節點
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析sql節點
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析statement 
    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);
  }
}

1.cache節點

        它經過調用CacheBuilder的相應方法完成cache的建立。每一個cache內部都有一個惟一的ID,這個id的值就是namespace。建立好的cache對象存入configuration的cache緩存中(該緩存以cache的ID屬性即namespace爲key,這裏再次體現了mybatis的namespace的強大用處)。sql

/**
 * cache- 配置本定命名空間的緩存。
 *         type- cache實現類,默認爲PERPETUAL,可使用自定義的cache實現類(別名或完整類名皆可)
 *         eviction- 回收算法,默認爲LRU,可選的算法有:
 *             LRU– 最近最少使用的:移除最長時間不被使用的對象。
 *             FIFO– 先進先出:按對象進入緩存的順序來移除它們。
 *             SOFT– 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。
 *             WEAK– 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。
 *         flushInterval- 刷新間隔,默認爲1個小時,單位毫秒
 *         size- 緩存大小,默認大小1024,單位爲引用數
 *         readOnly- 只讀
 * @param context
 * @throws Exception
 */
private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

2.cache-ref節點

        cacheRefElement方法負責解析cache-ref元素,它經過調用CacheRefResolver的相應方法完成cache的引用。建立好的cache-ref引用關係存入configuration的cacheRefMap緩存中。數據庫

/**
 *  cache-ref–從其餘命名空間引用緩存配置。
 *         若是你不想定義本身的cache,可使用cache-ref引用別的cache。
 *         由於每一個cache都以namespace爲id,
 *         因此cache-ref只須要配置一個namespace屬性就能夠了。
 *         須要注意的是,若是cache-ref和cache都配置了,以cache爲準。
 * @param context
 */
private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

3.resultMap節點

        resultMapElement方法負責解析resultMap元素,它經過調用ResultMapResolver的相應方法完成resultMap的解析。resultMap節點下除了discriminator子節點的其餘子節點都會解析成對應的ResultMapping對象,而每一個<resultMap>節點都會被解析成一個ResultMap對象,建立好的resultMap存入configuration的resultMaps緩存中(該緩存以namespace+resultMap的id爲key,這裏再次體現了mybatis的namespace的強大用處)。緩存

private void resultMapElements(List<XNode> list) throws Exception {
  for (XNode resultMapNode : list) {
    try {
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
  return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  String id = resultMapNode.getStringAttribute("id",
      resultMapNode.getValueBasedIdentifier());
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));
  String extend = resultMapNode.getStringAttribute("extends");
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

4.sql節點解析

sql節點用來定義可重用的sql語句片斷, sqlElement方法負責解析sql元素。id屬性用於區分不一樣的sql元素,在同一個mapper配置文件中能夠配置多個sql元素。mybatis

private void sqlElement(List<XNode> list) throws Exception {
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      // 記錄到sqlFragments中保存,其實 構造函數中能夠看到該字段指向了configuration的sqlFragments集合中
      sqlFragments.put(id, context);
    }
  }
}

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
  if (requiredDatabaseId != null) {
    if (!requiredDatabaseId.equals(databaseId)) {
      return false;
    }
  } else {
    if (databaseId != null) {
      return false;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    if (this.sqlFragments.containsKey(id)) {
      XNode context = this.sqlFragments.get(id);
      if (context.getStringAttribute("databaseId") != null) {
        return false;
      }
    }
  }
  return true;
}

2、XMLStatementBuilder

映射配置文件中還有一類比較重要的節點須要解析,其實就是select|insert|update|delete 節點,這些節點主要用於定義SQL語句,他們不在由XMLMapperBuilder進行解析,而是由XMLStatementBuilder負責進行解析,每一個節點會被解析成MappedStatement對象並存入到configuration對象中去。在這個方法內有幾個重要的步驟,理解他們對正確的配置statement元素頗有幫助。app

1.MappedStatement

MappedStatement包含了這些節點的不少屬性,其中比較重要的以下:函數

private String resource;//節點中的id 包括命名空間
private SqlSource sqlSource;//SqlSource對象,對應一條SQL語句
private SqlCommandType sqlCommandType;//SQL的類型,insert,delete,select,update

解析過程代碼以下:

public void parseStatementNode() {
  // 獲取sql節點的id以及databaseId若是和當前不匹配不加載改節點,
  // 若是存在id相同且databaseId不爲空的節點也不在加載改節點
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  // 獲取節點的多種屬性
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultType = context.getStringAttribute("resultType");
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  // 根據節點的名稱設置sqlCommandType的類型
  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // 在解析SQL語句以前先處理include節點
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // 處理selectKey節點
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // 完成節點的解析 該部分是核心
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // 獲取resultSets keyProperty keyColumn三個屬性
  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  // 獲取selectKey節點對應的selectKeyGenerator的id
  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;
  }

  // 經過MapperBuilderAssistant建立MappedStatement對象,
  // 並添加到configuration.mappedStatements集合中保存
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

二、解析include節點

在解析statement節點以前首先經過XMLIncludeTransformer解析include節點改過程會將include節點替換<sql>節點中定義的sql片斷,並將其中的${xx}佔位符換成真實的參數,

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  if (source.getNodeName().equals("include")) {  // ---(2)處理include節點
    // 查找refid屬性指向的<sql>,返回的是深克隆的Node對象
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
    Properties toIncludeContext = getVariablesContext(source, variablesContext);
    //遞歸處理include節點
    applyIncludes(toInclude, toIncludeContext, true);
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    // 將<include>節點替換<sql>節點
    source.getParentNode().replaceChild(toInclude, source);
    while (toInclude.hasChildNodes()) {
      // 將<sql>節點的子節點添加到<sql>節點的前面
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    // 替換後刪除<sql>節點
    toInclude.getParentNode().removeChild(toInclude);
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {// ---(1)
    if (included && !variablesContext.isEmpty()) {
      // replace variables in attribute values
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {// 遍歷當前sql的子節點
        Node attr = attributes.item(i);
        attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
      }
    }
    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()) {// ---(3)
    // replace variables in text node 替換對應的佔位符
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

三、解析selectKey節點

在insert,update節點中能夠定義selectKey節點來解決主鍵自增問題。

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()));
  String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
  String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
  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;

  // 生成SqlSource
  SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
  // selectKey節點中只能配置select語句
  SqlCommandType sqlCommandType = SqlCommandType.SELECT;

  // 建立MappedStatement對象,並添加到configuration的mappedStatements集合中保存
  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);
  // 建立對應的KeyGenerator(主鍵自增策略),添加到configuration中
  configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

3、綁定Mapper接口

每一個映射配置文件的命名空間能夠綁定一個Mapper接口,並註冊到MapperRegistry中。

// 綁定mapper接口
private void bindMapperForNamespace() {
  //獲取映射文件的命名空間
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      // 解析命名空間對應的類型 即dao
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {// 是否已經加載了
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        //註冊
        configuration.addLoadedResource("namespace:" + namespace);
        configuration.addMapper(boundType);
      }
    }
  }
}
相關文章
相關標籤/搜索