一、mybatis啟動分析

本專輯將介紹mybatis的原理和源碼分析。

1、概述

在不使用spring的情況下,我們從官網上可以看到mybatis的使用方法:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

我們可以看到,mybatis啟動,首先要加載自己的配置文件,通過配置文件創建SqlSessionFactory,然后再使用SqlSessionFactory對象創建SqlSession對象,通過SqlSession對象來操作數據庫。本篇先介紹mybatis加載配置文件的過程。

2、SqlSessionFactoryBuilder

首先我們看一下上面例子中SqlSessionFactoryBuilder的build()方法,我們找到這個方法的最終實現:

  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創建SqlSessionFactory,是直接實例化了一個DefaultSqlSessionFactory對象,在創建DefaultSqlSessionFactory對象的時候需要一個Configuration對象作為構造方法的參數,我們也可以看到Configuration對象是由XMLConfigBuilder讀取mybatis-config.xml這個配置文件的信息創建的,我們可以看一下具體的創建過程,我們看一下XMLConfigBuilder的parse()方法:

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      // 第一步,加載properties節點,把配置文件中定義配置變量解析出來
      propertiesElement(root.evalNode("properties"));
      // 第二步,加載setting節點,
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      // 第三步,加載類的別名
      typeAliasesElement(root.evalNode("typeAliases"));
      // 第四步,加載插件
      pluginElement(root.evalNode("plugins"));
     // 第五步,加載objectFactory節點
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 第八步,將前面settings節點解析出來的配置設置到Configuration中
      settingsElement(settings);
      // 第九步,加載environments節點,
      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);
    }
  }

我們可以看到,mybatis加載配置文件的時候,以configuration節點作為根節點開始解析,首先,加載properties節點,將properties下定義的變量加載到Configuration的variables屬性,具體實現比較簡單,這里不看源碼,有興趣的朋友可以自己看一下。
第二步,解析settings節點,把配置解析成Properties對象,在mybatis運行的過程中,可以通過修改這些屬性來改變mybatis 的行為,具體有哪些配置可以參考Mybatis的文檔。這里對vfsImpl參數有個特殊處理,可以指定VFS的實現。
第三步,加載類的別名信息,注冊到Configuration的typeAliasRegistry對象中,這個typeAliasRegistry對象中也包含了許多默認的類的別名,如:registerAlias("string", String.class)。注冊有兩種方式,一種是在<typeAliases></typeAliases>中添加<typeAlias alias="" type=""/>節點,另一種是通過添加<package name="domain.blog"/>直接指定一個包,掃描包中的@Alias("")注解來尋找別名。這些都可以從源碼中體現,具體實現源碼不再貼出。
第四步,加載mybatis的插件(Interceptor),這里把定義的插件信息讀出來,通過反射創建實例,然后注冊到Configuration的interceptorChain中。
第五步,加載自定義的ObjectFactory,ObjectFactory是mybatis查詢出結果后,創建查詢結果使用的對象工廠,默認是直接使用目標類的構造方法進行創建(具體實現可以看一下DefaultObjectFactory這個類),這里用戶可以自定義實現。這里把用戶自定義的ObjectFactory實現類注冊到Configuration的objectFactory屬性。
第六步和第七步,分別是加載objectWrapperFactory和reflectorFactory節點,具體過程和加載ObjectFactory節點類似,而且這兩個節點在官方文檔中沒有提及,所以我們大可不必看這兩個過程。
第八步,將前面settings節點解析出來的配置設置到Configuration中。
第九步,加載environments節點,environments節點下配置了不同環境下的不同的environment節點,environment節點主要配置了transactionManager和dataSource信息。我們可以看一下具體實現:

  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

我們可以看到,在加載environment的時候,只會去讀取default屬性指定的environment節點。environment節點下必須要有transactionManager和dataSource節點,不然讀取的時候會拋出異常。讀取完這些信息之后,會創建Environment對象,并將該對象設置到Configuration的environment屬性。
第十步,加載databaseIdProvider節點,databaseIdProvider的具體作用可以看一下mybatis官方文檔,我們可以看一下具體實現:

  private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // awful patch to keep backward compatibility
      if ("VENDOR".equals(type)) {
          type = "DB_VENDOR";
      }
      Properties properties = context.getChildrenAsProperties();
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }

如果定義了databaseIdProvider,mybatis會根據上面environment定義的datasource來選擇會用到的databaseId,并設置到configuration的databaseId屬性,以供后面加載statements使用。

第十一步,加載typeHandlers,typeHandlers的功能可以看一下mybatis的官方文檔。其作用為:在預處理語句(PreparedStatement)中設置一個參數時,或者從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。解析的最終結果會被注冊到Configuration的typeHandlerRegistry中,mybatis定義了很多類型的默認實現,有興趣的可以看一下源碼。typeHandler也可以通過注解的方式定義,這里就不再多說了。
最后一步是加載mappers節點,我們先來看一下解析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子節點的時候,解析方法會被分成兩大類,一種是從xml文件中解析(url、source),一種是從Class解析(class、package)。從Class解析比較簡單,僅僅是將Class注冊到Configuration的mapperRegistry屬性中。而解析xml比較復雜,我們來看一下具體過程,其中url和resource通過xml方式定義mapper(也就是我們平時使用的XXXMapper.xml文件),解析xml定義的mapper是交給XMLMapperBuilder來完成的,我們看一下XMLMapperBuilder的parse()方法:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 加載mapper節點下的所有元素
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

第一步,加載mapper節點下的所有元素,我們看一下configurationElement()方法的具體實現:

  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. Cause: " + e, e);
    }
  }

首先獲取mapper的namespace屬性,然后將namespace暫存到builderAssistant中,因為在接下來的過程中會頻繁用到namespace屬性。
接下來解析cache-ref和cache屬性,這里很簡單,就是單純的解析節點的屬性而已,所以不再贅述,如果不知道cache-ref和cache節點定義的作用的讀者,建議去官方文檔了解一下這兩個標簽的作用。
然后是加載parameterMap節點,官方文檔已經將這個配置標記為廢棄,所以我們可以不用關注這個,我們看一下接下來的解析resultMap的過程。

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

我們可以看到,解析resultMap節點時,首先會解析出id屬性,然后再解析出type屬性(“ofType”、“resultType”、“javaType”),然后是解析extend屬性、autoMapping屬性。
接下來就是解析resultMap下的子節點,加載子節點的時候對constructor和discriminator會做特殊處理。constructor和其他的節點一樣,都會被解析成一個ResultMapping對象,并加到一個list中。我們可以看一下ResultMapping又那些參數:

  private Configuration configuration;
  private String property;
  private String column;
  private Class<?> javaType;
  private JdbcType jdbcType;
  private TypeHandler<?> typeHandler;
  private String nestedResultMapId;
  private String nestedQueryId;
  private Set<String> notNullColumns;
  private String columnPrefix;
  private List<ResultFlag> flags;
  // 一個<result>節點的column屬性中包含多個column的話會被加載成composites
  private List<ResultMapping> composites;
  private String resultSet;
  private String foreignColumn;
  private boolean lazy;

加載邏輯比較簡單,這里我們不在贅述,有興趣的同學可以看一下具體實現。
我們再看一下加載discriminator節點為Discriminator對象的邏輯:

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class<?> javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    Map<String, String> discriminatorMap = new HashMap<String, String>();
    for (XNode caseChild : context.getChildren()) {
      String value = caseChild.getStringAttribute("value");
      String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
      discriminatorMap.put(value, resultMap);
    }
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
  }

這里也很簡單,分別讀取column、javaType、jdbcType、typeHandler屬性,然后讀取子節點case節點,把不同的value對應的resultMap解析成一個map,之后創建成一個Discriminator對象返回。
最后,系統會將resultMap節點解析出來的各種屬性封裝成一個ResultMap注冊到configuration中。到此,resultMap節點的解析我們就看完了。
我們接下里回到configurationElement()方法中來,在解析完resultMap節點后,接下里會解析sql節點,這里就是生成一個Map,key是sql的id,value是一個XNode節點的對象,解析過程比較簡單,我們就不單獨看了。
最后就是解析select、insert、update、delete這些節點了,我們可以看一下buildStatementFromContext()方法的實現,我們一層一層最終,最后可以發現,這些節點是交給XMLStatementBuilder的parseStatementNode()方法來解析的,我們先把源碼貼出:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

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

    // 此處省略部分代碼

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

    // 此處省略部分代碼

    // 解析節點的子節點
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析SelectKey節點
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // 將節點內容解析成SqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
   
    // 此處省略部分代碼

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

解析這些節點屬性的方法都很簡單,大家可以看一下源碼具體實現,上面的源碼省略了這些過程。而解析這些標簽子節點的內容這塊比較復雜,我們一起來看一下。我們很容易發現,解析節點內容是通過XMLIncludeTransformer的applyIncludes()方法實現的,我們貼出源碼來分析:

  public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    if (configurationVariables != null) {
      variablesContext.putAll(configurationVariables);
    }
    applyIncludes(source, variablesContext, false);
  }

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      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()) {
      // replace variables ins all text nodes
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

我們可以看到,這是一個遞歸方法,當加載的節點是include節點,或者節點屬性是ELEMENT_NODE的時候,會遞歸執行,直到節點屬性是TEXT_NODE節點。這里我們需要了解一下java解析xml的一些知識,我們舉個例子:
有一個xml串:

<a>
  head
  <b></b>
  tail
<a/>

在解析a標簽的子Node的時候,會解析出三個子Node,也就是說 head和tail都會被解析成一個Node,其類型為TEXT_NODE,而<b>節點會被解析成ELEMENT_NODE節點。
好了,我們再回到mybatis的解析過程中來。我們先看一下TEXT_NODE解析的過程,這個過程只有在included這個參數傳入true的時候才會被觸發。我們先來看看這個過程做了什么,這個過程是根據我們之前configuration節點下的properties節點解析出的變量,來替換源text文本中的變量(變量以$()這種方式表示),替換過程比較復雜,我們也沒必要細看,這里就不多說,我們知道只要知道在解析TEXT_NODE節點的時候會做一個變量替換的過程即可。
而解析ELEMENT_NODE的過程很簡單,就是遞歸調用而已,我們也不多說。
最后我們看一下解析include節點,這一步其實也比較簡單,就是把當前的include節點替換成include的目標節點。

我們再回到parseStatementNode()方法,在解析完子節點后,mybatis會處理SelectKey節點,具體方法是:processSelectKeyNodes(),處理過程可以概述為:將SelectKey節點解析成一個MappedStatement對象,然后再將MappedStatement對象封裝成SelectKeyGenerator對象,然后根據String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;這個規則生成一個id作為SelectKeyGenerator的id,注冊到configuration對象的keyGenerators屬性中,最后再把這個SelectKey節點移除,這里我們不貼出源碼了,有興趣的同學可以自己看一下。

接下來我們看到mybatis通過這種方式將節點內容解析成對象:

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

LanguageDriver有兩個實現類:RawLanguageDriver和XMLLanguageDriver。RawLanguageDriver的注釋中寫到不推薦使用RawLanguageDriver,除非確定要解析的sql是非動態sql,這里我們只看XMLLanguageDriver的實現就行了。我們一步一步追蹤,會發現,創建SqlSource 的過程是交給XMLScriptBuilder類的parseScriptNode()方法實現的,我們來看一下:

  public SqlSource parseScriptNode() {
    List<SqlNode> contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

  List<SqlNode> parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlers(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return contents;
  }

我們可以看到,mybatis會將標簽的子節點解析成一個SqlNode的List,如果子節點是TEXT_NODE和CDATA_SECTION_NODE,則直接封裝成一個TextSqlNode,如果是ELEMENT_NODE節點,說明是動態sql節點,這里使用NodeHandler來生成SqlNode對象。我們可以先來看一下SqlNode有多少子類:


SqlNode子類

這些子類正好對應我們使用mybatis動態sql的時候用的節點標簽。SqlNode的具體作用我們留到后面分析,我們暫且可以把SqlNode的作用理解為一個sql對象,這個對象可以根據輸入內容生成對應的sql語句。看到這里,我們可以猜測一下,在執行動態sql構建的時候,動態sql的構建是根據List<SqlNode>一步一步創建sql拼接而成的。其實我們從接下里的SqlSource構建過程也可以看出,所有的SqlNode節點會被封裝成一個MixedSqlNode節點,這個節點算是所有的SqlNode的一個外觀節點:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

生成的MixedSqlNode對象會作為SqlSource的構造方法參數被傳入SqlSource,到這里,SqlSource的創建過程我們分析完了。我們繼續回到上文提到的parseStatementNode()方法,在解析完各種屬性,創建完SqlSource對象后,mybatis會根據解析完的參數生成一個MappedStatement對象添加到Configuration對象的mappedStatements屬性中,mappedStatements是一個Map,key是節點的id(id屬性會被替換成currentNamespace + "." + base)屬性,value就是MappedStatement對象。
從上面的分析,我們看到了mapper節點的解析過程,接下來我們繼續回到XMLMapperBuilder的parse()方法,解析完mapper節點之后,mybatis會根據mapper的namespace去尋找對應的Mapper接口,并把接口加載到Configuration的mapperRegistry屬性中。這些操作在bindMapperForNamespace()方法中有體現。做完這些動作,parse()方法會調用下面三個方法做一些后續動作,這里我們不再進行詳細分析。大該的作用就是將未注冊的ResultMap、CacheRefs、Statements注冊到Configuration中。

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

到這里Configuration的創建過程分析完了,大家是不是感到很亂?我把Configuration的幾個比較重要的屬性加了注釋,來方便大家理解:

public class Configuration {

    /**
     * 加載配置文件中的<environment></environment>節點產生的對象,
     * 對象持有transactionFactory和dataSource
     */
    protected Environment environment;

    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls;
    protected boolean useActualParamName = true;
    protected boolean returnInstanceForEmptyRow;

    protected String logPrefix;
    protected Class <? extends Log> logImpl;
    protected Class <? extends VFS> vfsImpl;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
    protected Integer defaultStatementTimeout;
    protected Integer defaultFetchSize;
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

    /**
     * <properties></properties>節點解析出的值
     */
    protected Properties variables = new Properties();
    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();

    /**
     * 解析的<objectFactory></objectFactory>節點生成的objectFactory對象,有默認值
     */
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected boolean lazyLoadingEnabled = false;
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

    /**
     * 根據environment節點下配置的DataSource獲取的數據源id
     */
    protected String databaseId;
    /**
     * Configuration factory class.
     * Used to create Configuration for loading deserialized unread properties.
     *
     * @see <a >Issue 300 (google code)</a>
     */
    protected Class<?> configurationFactory;

    /**
     * 通過類方式(或者掃描包)方式注冊的Mapper類接口和通過xml文件注冊的namespace對應的接口
     */
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

    /**
     * 解析的<plugins></plugins>節點注冊的插件
     */
    protected final InterceptorChain interceptorChain = new InterceptorChain();

    /**
     * /mapper/parameterMap節點下的typeHandler屬性,默認會有一些通用的typeHandler
     */
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
    /**
     *<typeAliases></typeAliases>節點解析出來的別名信息
     */
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    /**
     * 解析的insert、update、delete、select節點,id為 namespace + "." + id
     */
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
    protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");

    /**
     * 解析的<resultMap></resultMap>節點,id為 namespace + "." + id
     */
    protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");

    /**
     * 解析的<parameterMap></parameterMap>節點,id為 namespace + "." + id
     */
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");

    /**
     * 解析的insert、update、delete、select節點中的<SelectKey></SelectKey>節點,
     * key是父節點id(insert、update、delete、select這些節點的id)+"!selectKey"
     */
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");

    /**
     * 已經加載的資源,(xml文件和Class,xml文件添加namespace,Class添加類名)
     */
    protected final Set<String> loadedResources = new HashSet<String>();
    protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();

    /*
     * A map holds cache-ref relationship. The key is the namespace that
     * references a cache bound to another namespace and the value is the
     * namespace which the actual cache is bound to.
     */
    protected final Map<String, String> cacheRefMap = new HashMap<String, String>();
}

SqlSessionFactoryBuilder創建SqlSessionFactory的過程其實最主要是解析配置并生成Configuration對象的過程,創建完Configuration對象之后,SqlSessionFactoryBuilder直接調用DefaultSqlSessionFactory的構造方法來創建SqlSessionFactory對象。

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

到此我們對Mybatis啟動過程中加載配置文件有了一定的了解,后面我們會繼續分析mybatis的源碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • 1. 簡介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存儲過程以及高級映射的優秀的...
    笨鳥慢飛閱讀 5,571評論 0 4
  • 1 引言# 本文主要講解JDBC怎么演變到Mybatis的漸變過程,重點講解了為什么要將JDBC封裝成Mybait...
    七寸知架構閱讀 76,574評論 36 980
  • 當我們學習 CSS 的時候,總是會聽到一個盒模型的概念。它是 CSS 的基礎,如果你不能理解盒模型,那你就無法學好...
    壽木閱讀 409評論 0 0
  • 午后陽光,照在一顆顆隨風搖曳的銀杏樹上,格外美麗,就這樣漫無目的的走走停停、看看想想! 已經很久沒有這...
    木棉花shelly閱讀 364評論 0 0
  • 不知不覺間,邁入了三十歲大關,關于成長的所有記憶悉數涌來。古人有言說,三十而立,而反觀此時的自己,家未成,業未立,...
    大野澤的風閱讀 1,509評論 6 7