Mybatis源码阅读(二)

493 阅读2分钟

摘要

上篇文章主要讲解了加载mybaits主配置文件的过程。本篇文章会接下去分析,Myabtis是如何加载mapper.xml配置文件的。

加载mapper配置文件

进入源码之前我们先认识几个类,先看下这个类图

  • SqlSouce: 接口,提供获取boundSql的方法,一条完整的sql语句,对应一个SqlSource对象
  • RawSqlSource:用来封装解析只包含#{}或者纯sql的sql标签内容
  • DynamicSqlSource: 用来封装和解析带有${}和动态sql标签的sql语句
  • PrivoderSqlSource: 用来封装和解析注解类型的sql语句
  • StaticSqlSource: 用来封装可以让JDBC直接执行的sql语句,上面几种SqlSource解析完成后就会生成一个StaticSqlSource对象

还有一个接口需要说明-SqlNode,在解析一个sql的过程中可能会有很多个动态标签,而每个标签会封装成一个对应的SqlNode添加到SqlSource中.

接着通过上节课说的XMLMapperBuilder 的parse方法解析mapper.xml配置文件,我们来看下具体的parse方法.

// mapper映射文件是否已经加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
      configurationElement(parser.evalNode("/mapper"));
      // 解析过的配置文件会添加到一个set集合中,避免重复解析
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();

这里面configurationElement方法就行用来解析文件的具体方法

/**
   * 解析mapper.xml映射文件
   * @param context 映射文件根节点<mapper>对应的XNode
   */
  private void configurationElement(XNode context) {
    try {
      // 获取<mapper>标签的namespace值
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 设置当前命名空间namespace的值
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析sql标签-就是对动态sql的重用,将写好的动态sql提取出来,然后在需要的地方进行调用
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析<select>\<insert>\<update>\<delete>子标签
      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);
    }
  }

其中buildStatementFromContext方法才是我们想看的去解析各个sql标签的具体方法

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // MappedStatement解析器
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
    	// 解析select等4个标签,创建MappedStatement对象
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

这里又有一个专门的解析类去将这个Sql解析成MappedStatement对象,MappedStatement对象封装着整个标签的信息,包括我们后面解析出来的SqlSource 具体的解析细节过程,大家点进去可以自己看到,我们拿几个点讲下

    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

这个过程是判断当前的数据源跟配置的数据源是否一直,不一致就不继续解析,而后面最主要的是需要看下SqlSource的解析过程

// 创建SqlSource,解析SQL
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

其中具体的解析过程在XMLLanguageDriver中通过XMLScriptBuilder的parseScriptNode方法解析

public SqlSource parseScriptNode() {
		// 获取解析过的SQL信息(解析动态SQL标签和${}),但是并没有对#{}进行处理
		MixedSqlNode rootSqlNode = parseDynamicTags(context);
		SqlSource sqlSource = null;
		// 如果包含${}和动态SQL语句,就是dynamic的
		if (isDynamic) {
			sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
		} else {
			// 否则是RawSqlSource的(带有#{}的SQL语句)
			sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
		}
		return sqlSource;
	}

接下来我们看下parseDynamicTags方法是怎么做的

protected MixedSqlNode parseDynamicTags(XNode node) {
		List<SqlNode> contents = new ArrayList<>();
		//获标签的子节点,子节点包括元素节点和文本节点
		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("");
				// 将文本内容封装到SqlNode中
				TextSqlNode textSqlNode = new TextSqlNode(data);
				// ${}是dynamic的
				if (textSqlNode.isDynamic()) {
					contents.add(textSqlNode);
					isDynamic = true;
				} else {
					// 除了${}都是static的,包括下面的动态SQL标签
					contents.add(new StaticTextSqlNode(data));
				}
				//处理元素节点
			} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
				String nodeName = child.getNode().getNodeName();
				// 动态SQL标签处理器
				NodeHandler handler = nodeHandlerMap.get(nodeName);
				if (handler == null) {
					throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
				}
				handler.handleNode(child, contents);
				// 动态SQL标签是dynamic的
				isDynamic = true;
			}
		}
		return new MixedSqlNode(contents);
	}

TextSqlNode封装含有${}的Sql文本,StaticTextSqlNode封装只有#{}或者纯sql的文本信息,其他动态标签会封装到对应的动态标签节点的SqlNode中,而contents是所有SqlNode的节点集合,而解析动态标签的Sql信息的时候会通过对应的标签从nodeHandlerMap获取对应标签的解析器去解析动态标签内的Sql信息.而nodeHandlerMap实在构造函数中调用initNodeHandlerMap方法进行初始化.

现在我们只看其中的一个动态标签处理器(IfHandler),其他的大概类似。

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            //递归解析该动态标签下的Sql语句
			MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
			//获取If标签中的Ognl表达式
			String test = nodeToHandle.getStringAttribute("test");
			IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
			targetContents.add(ifSqlNode);
}

在处理过程中就是递归调用解析方法去解析动态标签下面的Sql信息,然后将对应的SqlNode集合和if标签中的Ognl表达式封装成一个IfSqlNode。其他动态标签的解析夜大概类似。 最终解析完将对应的SqlSource返回然后封装成一个MappedStatement对象,添加到Configuration的Map容器中

// 通过构建者助手,创建MappedStatement对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

而最后这边Mybatis通过构建者助手,里面使用构造方法是创建MappedStatement对象,里面具体的构建过程,这里就不赘述,感兴趣的同学可以去点进去看下具体的构建过程.至此映射文件的解析已经全部完毕,所有信息都封装在Configuration的mappedStatements中,在后面执行的时候就会通过这个id(最终在构建的时候会id的拼接方式是(currentNamespace + "." + id)也就是加上空间名称用于区分)获取对应的MappedStatement拿到对应下信息进行数据库查询。

下一节,我们再一起去看下mybatis具体的执行过程,希望大家多多关注,或者有什么问题也可以留言探讨。