mybatis源碼配置文件解析之五:解析mappers標籤(解析XML映射文件)

在上篇文章中分析了mybatis解析<mappers>標籤,《mybatis源碼配置文件解析之五:解析mappers標籤 》重點分析了如何解析<mappers>標籤中的<package>子標籤的過程。mybatis解析<mappers>標籤主要完成了兩個操作,第一個是把對應的接口類,封裝成MapperProxyFactory放入kownMappers中;另一個是把要執行的方法封裝成MapperStatement。

一、概述

在上篇文章中分析了<mappers>標籤,重點分析了<package>子標籤,除了可以配置<package>子標籤外,在<mappers>標籤中還可以配置<mapper>子標籤,該子標籤可以配置的熟悉有resource、url、class三個屬性,解析resource和url的過程大致相同,看解析resource屬性的過程。

下面看部分代碼,

if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            /**
             * 處理mapper文件和對應的接口
             */
            mapperParser.parse();
          }

可以看到調用了XMLMapperBuilder的parse方法,

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //1、解析mapper文件中的<mapper>標籤及其子標籤,並設置CurrentNamespace的值,供下面第2步使用
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //2、綁定Mapper接口,並解析對應的XML映射文件
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

從上面的代碼中可以看出首先解析resource資源說代表的XML映射文件,然後解析XML;映射文件中的namespace配置的接口。

二、詳述

通過上面的分析知道,解析resource配置的XML映射文件,分為兩步,第一步就是解析XML映射文件的內容;第二步是解析XML映射文件中配置的namespace屬性,也就是對於的Mapper接口。

1、解析XML映射文件

看如何解析XML映射文件內容,也就是下面的這行代碼,

configurationElement(parser.evalNode("/mapper"));

看具體的實現,

/**
   * 解析XML映射文件的內容
   * @param context
   */
  private void configurationElement(XNode context) {
    try {
        //獲得namespace屬性
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      //設置到currentNamespace中
      builderAssistant.setCurrentNamespace(namespace);
      //解析<cache-ref>標籤
      cacheRefElement(context.evalNode("cache-ref"));
//二級緩存標籤 cacheElement(context.evalNode(
"cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/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); } }

 上面方法解析XML映射文件的內容,其中有個和二級緩存相關的配置,即<cache>標籤。那麼xml映射文件可以配置哪些標籤那,看下面,

在XML映射文件中可以配置上面的這些標籤,也就是上面方法中解析的內容。重點看解析select、update、delete、select。也就是下面這行代碼

//解析select、insert、update、delete子標籤
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

其方法定義如下,

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

這裡會校驗databaseId,如果自定義配置了,則使用自定義的,否則使用默認的,看方法buildStatementFromContext方法

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

調用XMLStatementBuilder的parseStatementNode方法

/**
 * 解析select、update、delete、insert標籤
 */
  public void parseStatementNode() {
    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);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //查詢語句默認開啟一級緩存,這裡默認是true
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //生成SqlSource,這裡分兩種,DynamicSqlSource和RawSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    //例,id="selectUser"
    //這裡的keyStatementId=selectUser!selectKey
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    //keyStatementId=cn.com.dao.userMapper.selectUser!selectKey
    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;
    }

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

上面的代碼主要是解析標籤中的各種屬性,那麼標籤中可以配置哪些屬性那,下面看select標籤的屬性,詳情可參見//www.w3cschool.cn/mybatis/f4uw1ilx.html

上面是select標籤中可以配置的屬性列表。

上面的代碼重點看以下重點

二級緩存

下面看和緩存相關的

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //查詢語句默認開啟一級緩存,這裡默認是true
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);

這裡僅針對select查詢語句使用緩存,這裡的默認不會刷新緩存flushCache為false,默認開啟緩存useCache為ture,這裡的緩存指的是一級緩存,經常說的mybatis一級緩存,一級緩存是sqlSession級別的。

看完了一級緩存,下面看SqlSource的內容

SqlSource

下面是SqlSource相關的,

//生成SqlSource,這裡分兩種,DynamicSqlSource和RawSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

上面是生成SqlSource的過程,

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

看parseScriptNode方法

public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
        //含義${}符合的為DynamicSqlSource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        //不含有${}的為rawSqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

從上面的代碼可以看到在映射文件中根據參數佔位符的標識符(${}、#{})分為DynamicSqlSource和RawSqlSource。具體如何判斷,後面詳細分析。

addMappedStatement

最後看builderAssistant.addMappedStatement方法,

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    //cn.com.dao.UserMapper.selectUser
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    /*向mappedStatements字段中加入MappedStatement,這裡會加入兩個key
     * cn.com.dao.UserMapper.selectUser statement
     * selectUser statement
     * 每次都會插入上面的兩種key,兩種key對應的value都是同一個statement
     * 
     */
    configuration.addMappedStatement(statement);
    return statement;
  }

該方法主要完成的功能是生成MappedStatement,且放入configuration中。

2、解析namespace屬性

上面分析了解析XML映射文件的內容的過程,最後的結果是把XML映射文件中的select、update、insert、delete標籤的內容解析為MappedStatement。下面看解析XML映射文件中的namespace屬性,

//2、綁定Mapper接口,並解析對應的XML映射文件
      bindMapperForNamespace();

上面我給的注釋是綁定接口並解析對應的XML映射文件,這個方法沒有參數,怎麼綁定具體的接口並解析對應的映射文件那,

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
          //加載類,這裡加載的是mapper文件中配置的namespace配置的接口
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {//判斷該接口是否被加載過,在mapperRegistry中的knowsMapper中判斷
          // 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
            //把該接口作為已加載的資源存放到loadedResources中,loadedResources存放的是已加載的mapper文件的路徑
          configuration.addLoadedResource("namespace:" + namespace);
          //把該接口放到mapperRegistry中的knowsMapper中,並解析該接口,根據loadedResources判定是否需要解析相應的XML映射文件
          configuration.addMapper(boundType);
        }
      }
    }
  }

獲得builderAssistant.getCurrentNamespace(),在解析XML映射文件時,第一步便是設置該屬性,這裡用到的便是上一步中設置的那個XML映射文件中的namespace屬性值。獲得該接口的名稱,判斷是否生成過MapperProxyFactory,即放入過knownMappers中,看configuration.hasMapper方法,

public boolean hasMapper(Class<?> type) {
    return mapperRegistry.hasMapper(type);
  }
public <T> boolean hasMapper(Class<T> type) {
    return knownMappers.containsKey(type);
  }

如果在knownMappers中,則不進行解析,如果不在才進行下面的邏輯處理,調用configuration.addLoadedResource方法,放入loadedResources中,標識在第一步已經解析過對應的XML映射文件;調用configuration.addMapper方法,解析該接口,這個過程和在<mapper>標籤中配置class屬性的過程是一樣的,後面詳細分析。

三、總結

本文分析了mappers標籤中mapper子標籤中resource和url屬性的解析過程,首先解析對應的XML映射文件,解析的結果為MappedStatement對象,然後解析其namespace對應的接口,解析的結果為MapperProxyFactory對象。

 

有不當之處,歡迎指正,感謝!

Tags: