源碼分析Mybatis MappedStatement的建立流程

上文源碼分析Mybatis MapperProxy建立流程重點闡述 MapperProxy 的建立流程,但並無介紹 *.Mapper.java(UserMapper.java) 是如何與 *Mapper.xml 文件中的 SQL 語句是如何創建關聯的。本文將重點接開這個謎團。java

接下來重點從源碼的角度分析Mybatis MappedStatement的建立流程。node

一、上節回顧

咱們注意到這裏有兩三個與Mapper相關的配置:sql

  1. SqlSessionFactory#mapperLocations,指定xml文件的配置路徑。
  2. SqlSessionFactory#configLocation,指定mybaits的配置文件,該配置文件也能夠配置mapper.xml的配置路徑信息。
  3. MapperScannerConfigurer,掃描Mapper的java類(DAO)。

咱們已經詳細介紹了Mybatis Mapper對象的掃描與構建,那接下來咱們將重點介紹MaperProxy與mapper.xml文件是如何創建關聯關係的。緩存

根據上面的羅列以及上文的講述,Mapper.xml與Mapper創建聯繫主要的入口有三:mybatis

1)MapperScannerConfigurer掃描Bean流程中,在調用MapperReigistry#addMapper時若是Mapper對應的映射文件(Mapper.xml)未加載到內存,會觸發加載。併發

2)實例化SqlSessionFactory時,若是配置了mapperLocations。app

3)示例化SqlSessionFactory時,若是配置了configLocation。ide

本節的行文思路:從SqlSessionFacotry的初始化開始講起,由於mapperLocations、configLocation都是是SqlSessionFactory的屬性。源碼分析

>舒適提示:下面開始從源碼的角度對其進行介紹,你們能夠先跳到文末看看其調用序列圖。fetch

二、SqlSessionFacotry

if (xmlConfigBuilder != null) {  // XMLConfigBuilder   // [@1](https://my.oschina.net/u/1198)
      try {
        xmlConfigBuilder.parse();

        if (logger.isDebugEnabled()) {
          logger.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

if (!isEmpty(this.mapperLocations)) {   // @2
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (logger.isDebugEnabled()) {
          logger.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

上文有兩個入口: 代碼@1:處理configLocation屬性。 代碼@2:處理mapperLocations屬性。

咱們先從XMLConfigBuilder#parse開始進行追蹤。該方法主要是解析configLocation指定的配置路徑,對其進行解析,具體調用parseConfiguration方法。

2.1 XMLConfigBuilder

咱們直接查看其parseConfiguration方法。

private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      settingsElement(root.evalNode("settings"));
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));   // [@1](https://my.oschina.net/u/1198)
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

重點關注mapperElement,從名稱與參數便可以看出,該方法主要是處理中mappers的定義,即mapper sql語句的解析與處理。若是使用過Mapper的人應該不難知道,咱們使用mapper節點,經過resource標籤訂義具體xml文件的位置。

2.1.1XMLConfigBuilder#mapperElement

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          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());    // @1
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<!--?--> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

上面的代碼比較簡單,不難看出,解析出Mapper標籤,解析出resource標籤的屬性,建立對應的文件流,經過構建XMLMapperBuilder來解析對應的mapper.xml文件。此時你們會驚訝的發現,在SqlSessionFacotry的初始化代碼中,處理mapperLocations時就是經過構建XMLMapperBuilder來解析mapper文件,其實也不難理解,由於這是mybatis支持的兩個地方可使用mapper標籤來定義mapper映射文件,具體解析代碼固然是同樣的邏輯。那咱們解析來重點把目光投向XMLMapperBuilder。

2.2 XMLMapperBuilder

XMLMapperBuilder#parse
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {     // @1
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();                                    // @2
    parsePendingChacheRefs();                                   // @3
    parsePendingStatements();                                     // @4
  }

代碼@1:若是該映射文件(*.Mapper.xml)文件未加載,則首先先加載,完成xml文件的解析,提取xml中與mybatis相關的數據,例如sql、resultMap等等。

代碼@2:處理mybatis xml中ResultMap。

代碼@3:處理mybatis緩存相關的配置。

代碼@4:處理mybatis statment相關配置,這裏就是本篇關注的,Sql語句如何與Mapper進行關聯的核心實現。

接下來咱們重點探討parsePendingStatements()方法,解析statement(對應SQL語句)。

2.2.1 XMLMapperBuilder#parsePendingStatements

private void parsePendingStatements() {
	  Collection<xmlstatementbuilder> incompleteStatements = configuration.getIncompleteStatements();
	  synchronized (incompleteStatements) {
		  Iterator<xmlstatementbuilder> iter = incompleteStatements.iterator();    // @1
		  while (iter.hasNext()) {
			  try {
				  iter.next().parseStatementNode();   // @2
				  iter.remove();
			  } catch (IncompleteElementException e) {
				  // Statement is still missing a resource...
			  }
		  }
	  }
  }

代碼@1:遍歷解析出來的全部SQL語句,用的是XMLStatementBuilder對象封裝的,故接下來重點看一下代碼@2,若是解析statmentNode。

2.2.2 XMLStatementBuilder#parseStatementNode

public void parseStatementNode() {
    String id = context.getStringAttribute("id");                                                                  // @1 start
    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);
    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);             // @1 end
    
    // Parse the SQL (pre: <selectkey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);                // @2
    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() &amp;&amp; SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

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

這個方法有點長,其關注點主要有3個: 代碼@1:構建基本屬性,其實就是構建MappedStatement的屬性,由於MappedStatement對象就是用來描述Mapper-SQL映射的對象。

代碼@2:根據xml配置的內容,解析出實際的SQL語句,使用SqlSource對象來表示。

代碼@3:使用MapperBuilderAssistant對象,根據準備好的屬性,構建MappedStatement對象,最終將其存儲在Configuration中。

2.2.3 Configuration#addMappedStatement

public void addMappedStatement(MappedStatement ms) {
   mappedStatements.put(ms.getId(), ms);
}

MappedStatement的id爲:mapperInterface + methodName,例如com.demo.dao.UserMapper.findUser。

即上述流程完成了xml的解析與初始化,對終極目標是建立MappedStatement對象,上一篇文章介紹了mapperInterface的初始化,最終會初始化爲MapperProxy對象,那這兩個對象如何關聯起來呢?

從下文可知,MapperProxy與MappedStatement是在調用具Mapper方法時,能夠根據mapperInterface.getName + methodName構建出MappedStatement的id,而後就能夠從Configuration的mappedStatements容器中根據id獲取到對應的MappedStatement對象,這樣就創建起聯繫了。

其對應的代碼:

// MapperMethod 構造器
public MapperMethod(Class<!--?--> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, method);
}

// SqlCommand 構造器
public SqlCommand(Configuration configuration, Class<!--?--> mapperInterface, Method method) throws BindingException {
      String statementName = mapperInterface.getName() + "." + method.getName();
      MappedStatement ms = null;
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      if (ms == null) {
        throw new BindingException("Invalid bound statement (not found): " + statementName);
      }
      name = ms.getId();
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }

怎麼樣,從上面的源碼分析中,你們是否已經瞭解MapperProxy與Xml中的SQL語句是怎樣創建的關係了嗎?爲了讓你們更清晰的瞭解上述過程,現給出其調用時序圖:

在這裏插入圖片描述


>做者介紹:丁威,《RocketMQ技術內幕》做者,RocketMQ 社區佈道師,公衆號:中間件興趣圈 維護者,目前已陸續發表源碼分析Java集合、Java 併發包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源碼專欄。

相關文章
相關標籤/搜索