天天看點

MyBatis源碼閱讀筆記1-xml檔案加載過程

mybatis 3.5.5的版本

按照mybatis給的文檔示例中,配置檔案的加載非常簡單(如下代碼所示),而且是使用mybatis的第一步,這篇文章探究mybais源碼加載配置檔案的全過程。按照代碼的執行順序進行介紹。

String resource = "org/mybatis/example/mybatis-config.xml";
//第一步
InputStream inputStream = Resources.getResourceAsStream(resource);        
//第二步
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);   
           

一、Resources類

Resources是一個工具類,用來讀我們的配置檔案為stream、reader、Properties、URL,在第一步裡我們就調用了它的getResourceAsStream方法,将配置檔案讀為stream格式。

在這個類中持有一個ClassLoadWrapper的包裝類對象,這個類對象中有一個通過getSystemClassLoader方法得到的系統級systemClassLoader,另外還有一個預設為null的defaultClassLoader,這兩個ClassLoader之後會用到。

getResourceAsStream方法會調用ClassLoadWrapper的getResourceAsStream方法,在這個方法中,會建立一個ClassLoader的數組按照從前到後的順序試着加載這個resource,調用的是ClassLoader.getResourceAsStream方法。

下面看具體代碼縮減(有一些方法和方法内的操作因為很好了解或無助于了解加載過程被省略了,以下的代碼段也類似):

class Resources{
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
//在一般情況下我們調用的是無classloader版本的,那麼這時候會傳進來null
public static InputStream getResourceAsStream(ClassLoader loader, String resource) {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    return in;
  }
}

class ClassLoaderWrapper {
ClassLoader defaultClassLoader;
  ClassLoader systemClassLoader;
//構造的時候defaultClassLoader為null
ClassLoaderWrapper() {
      systemClassLoader = ClassLoader.getSystemClassLoader();
}
//最終我們調用的是這個版本的getResourceAsStream方法,這個classloader數組來源于getClassLoaders方法
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {
        // 最終使用classloader的getResourceAsStream方法進行加載
        InputStream returnValue = cl.getResourceAsStream(resource);
        // 因為有一些classloader需要前面的反斜杠,又加上試一下
        if (null == returnValue) {
          returnValue = cl.getResourceAsStream("/" + resource);
        }
        if (null != returnValue) {
          return returnValue;
        }
      }
    }
    return null;
}
//因為我們通常不傳classloader,defaultClassLoader也為null,是以這個數組一般情況下前兩個為null
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,
        defaultClassLoader,
        Thread.currentThread().getContextClassLoader(),
        getClass().getClassLoader(),
        systemClassLoader};
  }
}
           

二、SqlSessionFactoryBuilder類

這個類使用了建構者模式來進行建立SqlSessionFactory的工作。其中我們調用的build方法主要完成兩項工作,一項是開始解析xml配置檔案,一項是重置ErrorContext的錯誤日志記錄(注意它是一個單例模式,把構造器private了。而且用static final的ThreadLocal來儲存ErrorContext對象保證線程安全,具體的不在下文中提及和分析,相關代碼段不會出現)。下面看源碼:

class SqlSessionFactoryBuilder{
    //我們首先調用這個方法,例子上environment和properties均為null
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //真正開始解析xml配置檔案的類
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      //錯誤日志
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
//解析完成後調用這個方法建立一個DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}
           

等解析完了之後會建立一個DefaultSqlSessionFactory的執行個體傳回,解析完成的結果就是這個Configuration對象。

三、XMLConfigBuilder類(解析整個config.xml檔案)

顧名思義,這個類是對xml格式的config進行解析的封裝類,其中會調用到其他的類,是解析的開端。

1.首先我們分析XMLConfigBuilder的父類BaseBuilder,其中存儲三個對象,分别是Configuration、TypeAliasRegistry、TypeHandlerRegistry,其中後兩個來自于Configuration對象,是以說BaseBuilder的關鍵就是存儲了Configuration對象并提供了一系列方法,其他繼承自BaseBuilder的Builder也自然擁有了這些。具體的繼承關系圖在其他文章中給出。

2.然後我們看一下XMLConfigBuilder中的屬性,分别是boolean parsed、final XPathParser parser、String environment、final ReflectorFactory loaclReflectorFactory。它們分别的作用是:parsed記錄是否已經解析過了,parser是用來具體對xml進行解析的工具類,environment是environment中default的值,loaclReflectorFactory是反射工廠,提供了預設的DefaultReflectorFactory實作。

3.然後我們結合代碼來看一下我們調用的構造方法:

//我們首先調用這個構造方法,後面兩個參數為null
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
//最終我們生成了一個XPathParser對象傳了進來
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
           

這裡注意一點,XPathParser是mybatis的一個類,其中封裝了很多方法,目的就是調用工具解析xml文檔,為後面使用提供友善。在這裡面我們主要儲存了doucument對象,也即文檔。

那麼在mybatis中,具體的xml文檔解析是用的javax.xml.xpath包和org.w3c.dom包提供的方法,具體mybatis的封裝可以閱讀org.apache.ibatis.parsing包内的内容。

在這裡還需要注意另外一點,Configuration對象在這裡建立了,它雖然沒有使用單例模式,但是實際上我們的使用的都是一個,我們在一個單獨的文章中分析一下Configuration對象,在這裡我們隻需要知道,我們解析出來的所有資訊都封裝在這個對象中。

4.下面我們分析一下接下來要調用的parse方法,它會設定parsed為true,然後調用另一個方法parseConfiguration,并把doucumnet中configuration的根标簽傳遞進去進行解析,下面我們看代碼:

//傳入節點即為configuration根節點
private void parseConfiguration(XNode root) {
    try {
      //解析properties标簽
      propertiesElement(root.evalNode("properties"));
      //解析settings标簽并轉化為properties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加載自定義的虛拟檔案系統api-vfs
      loadCustomVfs(settings);
      //同上,日志
      loadCustomLogImpl(settings);
      //下面這些都是加載某個标簽進行解析,就不過多介紹,有興趣可以自己看源碼
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      //這裡開始解析mappers标簽
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
           

5.我們對最後一個mapperElement方法進行分析,因為mapper标簽關聯着其他的xml的mapper檔案,是以我們具體來看一看。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      //挨個mapper标簽進行解析
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //對于package類型的進行解析
          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) {
            //對resource類型的進行解析
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //對url類型的進行解析
            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類型的進行解析
            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.");
          }
        }
      }
    }
  }
           

我們這裡以resource類型的來看一下接下來的解析過程,我們可以看到和剛開始一樣,同樣的套路,先取出stream,然後使用一個叫做XMLMapperBuilder的類進行解析。

四、XMLMapperBuilder類(解析整個mapper.xml檔案)

類似于上一個XMLConfigBuilder類,該XMLMapperBuilder類也是繼承自BaseBuilder,是以它同樣持有Configuration對象,并且在構造方法中利用super關鍵字進行指派。同樣類似的,因為解析Mapper檔案也是解析檔案,是以它也用到了XPathParser工具類。

1.首先我們還是看看它的屬性

private final XPathParser parser;
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  private final String resource;
           

其中MapperBuilderAssistant類是一個輔助類,也繼承自BaseBuilder,用來處理輔助性工作。Map對象sqlFragments是用來儲存sql片段的,用string也就是id建立映射關系,這裡需要稍微了解一下,這個map的實作是在Configuration中叫做StrictMap,它會同時加帶namespace和不帶namespace的兩種鍵值對。resource就是該mapper檔案的位址。

2.我們看一下具體的parse方法

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

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

3.我們具體看一下configurationElement方法的工作過程

//傳遞進來的就是mapper标簽,也就是根節點
private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      //必須要有namespace
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      //解析各個标簽
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      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);
    }
  }
           

4.我們重點關注一下buildStatementFromContext方法

private void buildStatementFromContext(List<XNode> list) {
    buildStatementFromContext(list, null);
  }

  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);
      }
    }
  }
           

我們看到在方法中其實把每一個select | update | insert | delete 标簽都用了一個XMLStatementBuilder對象進行了解析。

五、XMLStatementBuilder類(解析單個select | update | insert | delete标簽)

1.和前面兩個類一樣,XMLStatementBuilder也繼承自BaseBuilder類,同樣通過super關鍵字調用了上層構造器。我們來看一下它的構造方法和屬性值。

class XMLStatementBuilder{
  private final MapperBuilderAssistant builderAssistant;
  private final XNode context;
  private final String requiredDatabaseId;

public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
    super(configuration);
    this.builderAssistant = builderAssistant;
    this.context = context;
    this.requiredDatabaseId = databaseId;
  }
}
           

可以看到其實挺普通的,它的所有屬性都是從調用方給它傳過來的。其中requiredDataBaseId是關于資料庫辨別的,不同的資料庫有不同的值,對于mysql來說是null,我們并不關心。

2.我們再來看一下parseStatementNode方法

public void parseStatementNode() {
    //标簽id
    String id = context.getStringAttribute("id");
    //根據資料庫不同而變化
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //根據标簽名判斷是不是select标簽以及處理其他緩存和順序相關設定
    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标簽,也即動态sql的引用,需要在解析前處理
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //擷取parameterType值并解析類
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //MyBatis 從 3.2 版本開始支援插入腳本語言,這允許你插入一種語言驅動,并基于這種語言來編寫動态 SQL 查詢語句。
    //前面看到的所有 xml 标簽都由預設 MyBatis 語言提供,而它由語言驅動 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名為 xml)所提供。
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    //處理selectKey标簽,它會在insert、update、delete标簽中出現,需要在解析前處理
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    //下面的步驟為解析sql,其中selectKey和include标簽已經解析并且移除了
    //這裡處理主鍵key
    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;
    }

    //生成sqlsource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

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

在這裡有三個關鍵點:

一是include标簽的解析過程值得關注,因為它和sqlfragmnet是相關聯的;

二是sqlsource在這裡建立了并且傳遞給了MappedStatement;

三是最終MappedStatement放入了Configration中

當builderAssitant把MappedStatement加入進去,這個解析過程就算完成了,所有xml檔案資訊都存放在了Configuration對象中。

總地來說,從config.xml檔案解析的所有資訊都在Configuration對象中,所有的sql标簽在名為sqlFragments的Map中,所有标簽解析過後都在名為mappedStatements的Map中,在每個MappedStatement中都存有所有sql文本的封裝SqlSource,sqlSource又通過SqlNode對所有sql文本/标簽資訊進行了儲存和處理

繼續閱讀