一、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 href='https://code.google.com/p/mybatis/issues/detail?id=300'>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阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,423评论 0 4
  • 1 引言# 本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为什么要将JDBC封装成Mybait...
    七寸知架构阅读 76,423评论 36 980
  • 当我们学习 CSS 的时候,总是会听到一个盒模型的概念。它是 CSS 的基础,如果你不能理解盒模型,那你就无法学好...
    寿木阅读 392评论 0 0
  • 午后阳光,照在一颗颗随风摇曳的银杏树上,格外美丽,就这样漫无目的的走走停停、看看想想! 已经很久没有这...
    木棉花shelly阅读 345评论 0 0
  • 不知不觉间,迈入了三十岁大关,关于成长的所有记忆悉数涌来。古人有言说,三十而立,而反观此时的自己,家未成,业未立,...
    大野泽的风阅读 1,493评论 6 7