1. 程式人生 > >MyBatis的執行原理1:構建SqlSessionFactory過程

MyBatis的執行原理1:構建SqlSessionFactory過程

首先建立了一個SqlSessionFactoryBuilder物件,然後呼叫該物件的build方法載入全域性XML配置的流檔案構建出一個SqlSessionFactory物件。

//指定全域性配置檔案路徑
String resource = "org/mybatis/example/mybatis-config.xml";
//載入配置檔案
InputStream inputStream = Resources.getResourceAsStream(resource);
//構建者模式建立SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

檢視一下SqlSessionFactoryBuilder的原始碼:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      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.
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

SqlSessionFactoryBuilder只有一堆過載的build方法,除了build(Configuration)方法,其他方法的引數都是輸入流,最終由build(Configuration)方法生成SqlSessionFactory物件,下面來看如何構建Configuration物件。

構建XMLConfigBuilder物件

從XMLConfigBuilder類名就可以看出,這是用來解析XML配置檔案的類,其父類為BaseBuilder。 BaseBuilder還包含了MapperBuilderAssistant, SqlSourceBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLScriptBuilder, XMLStatementBuilder等子類,這些子類都是用來解析MyBatis各個配置檔案,這裡暫時只討論 XMLConfigBuilder:解析全域性配置檔案。

 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }

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

檢視XMLConfigBuilder原始碼,可以得知XML配置檔案最終是由org.apache.ibatis.parsing.XPathParser封裝的XPath解析的,並非我們熟悉的DOM和SAX,這裡不對XPath展開講解。通過XpathParser構造方法傳入我們讀取的XML流檔案、Properites流檔案和environment等引數得到了一個XpathParser例項物件parser,這裡parser已包含全域性XML配置檔案解析後的所有資訊,再將parser作為引數傳給XMLConfigBuilder構造方法。XMLConfigBuilder構造方法呼叫其父類BaseBuilder的構造方法BaseBuilder(Configuration),構造出一個XMLConfigBuilder物件。值得注意的是,這裡BaseBuilder構造方法引數是一個初始化的Configuration物件,Configuration物件初始化的時候,內建的別名註冊器TypeAliasRegistry註冊了預設的別名:

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

... 

所以XML配置檔案裡可以直接用這些別名。

此時我們已經得到了XMLConfigBuilder物件,再看SqlSessionFactoryBuilder的build方法,將XMLConfigBuilder例項物件parser呼叫parser()方法得到的Configuration例項物件config作為引數,呼叫SqlSessionFactory介面的實現類DefaultSqlSessionFactory構造出SqlSessionFactory物件。

構建Configuration物件

XMLConfigBuilder物件在呼叫parser()方法時,會讀出所有所有配置檔案,將配置檔案解析後儲存在Configuration物件中。

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //引數是<configuraton>標籤根節點
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

XMLConfigBuilder的parseConfiguration(XNode)方法把XML全域性配置檔案中每一個節點的資訊都讀取出來,儲存在一個Configuration物件中,Configuration分別對以下內容做出了初始化:

  • properties 屬性
  • settings 設定
  • typeAliases 類型別名
  • typeHandlers 型別處理器
  • objectFactory 物件工廠
  • plugins 外掛
  • environments 環境
  • databaseIdProvider 資料庫廠商標識
  • mappers 對映器 這裡對properties和mappers的初始化進行分析:

    properties的初始化

 private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }

XMLConfigBuilder的getChildrenAsProperties()方法讀取properties標籤的子節點,儲存到Configuration物件的Properties屬性裡,這裡可以看出只能在resource和url兩種方式中二選一來載入外部properties配置檔案,如果外部properties檔案裡面屬性名和主配置XML檔案properties標籤的子元素屬性重名,則會覆蓋主配置檔案的屬性值,然後將初始化的Configuration物件中的Properties與解析配置檔案後封裝好的Properties合併,最後再將Properties儲存到Configuration物件中。

mappers的初始化

 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());
            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.");
          }
        }
      }
    }
  }

遍歷mappers標籤下所有子節點

  • 如果遍歷到package子節點,是以包名引入對映器,則將該包下所有Class註冊到Configuration的mapperRegistry中。
  • 如果遍歷到mapper子節點的class屬性,則將制定的Class註冊到註冊到Configuration的mapperRegistry中。
  • 如果遍歷到mapper子節點的resource或者url屬性,則直接對資原始檔進行解析: 首先構建一個XMLMapperBuilder物件,構建過程如下
 public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }

XPathParser將mapper配置檔案解析成Document物件後封裝到一個XPathParser物件,再將XPathParser物件作為引數傳給XMLMapperBuilder構造方法並構造出一個XMLMapperBuilder物件,XMLMapperBuilder物件的builderAssistant欄位是一個MapperBuilderAssistant物件,同樣也是BaseBuilder的一個子類,其作用是對MappedStatement物件進行封裝。

有了XMLMapperBuilder物件後,就可以進入解析mapper對映檔案的過程:

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

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

呼叫XMLMapperBuilder的configurationElement方法,mapper對映檔案進行解析

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

mapper對映檔案必須有namespace屬性值,否則丟擲異常,將namespace屬性儲存到XMLMapperBuilder的MapperBuilderAssistant物件中,以便其他方法呼叫。 該方法對mapper對映檔案每個標籤逐一解析並儲存到Configuration物件中,其中buildStatementFromContext將mapper對映檔案中SQL語句解析成MappedStatement物件,儲存到Configuration的mappedStatements集合中。

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    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);
      }
    }
  }

使用XMLStatementBuilder將select、insert、update和delete節點的屬性及SQL語句封裝成MappedStatement物件

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

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

最後MapperBuilderAssistant將解析完成的資料封裝成MappedStatement物件放入Configuration物件的mappedStatements容器中,得到了最終的Configuration物件後傳入SqlSessionFactoryBuilder的構造方法,生成我們需要的SqlSessionFactory物件。