深入浅出Mybatis源码解析——SqlSource的创建流程

前言

在前一篇文章深入浅出Mybatis源码解析——映射文件加载流程中,最后说到了创建SqlSource和创建MappedStatement对象,由于篇幅原因最后只好终止了,所以便只好在写一篇文章来说说SqlSource这样的一个创建流程是怎样的,在本系列第一篇完成后,也曾问过一两个认识的读者,有说不太看得懂,可能本人写博客不够图文并茂,所以在下一篇博文中准备来整理一张Mybatis的一个完整的执行流程图,来带读者清楚的了解一下Mybatis的这样一个执行流程。

本篇文章即将开始,请系好安全带!

一、动态SQL标签处理器

再正式开始之前,先来看下createSqlSource方法的代码具体实现,代码如下:

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
	// 初始化了动态SQL标签处理器
	XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
	// 解析动态SQL
	return builder.parseScriptNode();
}

注意这个方法是在XMLLanguageDriver.java这个类中,既然我们看到了入口的地方,那就看看Mybatis的SqlSource是如创建的,上面的代码中首先构建了XMLScriptBuilder这个对象,我觉得有必要看看这个构造方法做了什么,因为我感觉这个构造函数不同凡响。废话不多说,直接上代码:

	public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
		super(configuration);
		this.context = context;
		this.parameterType = parameterType;
		// 初始化动态SQL中的节点处理器集合
		initNodeHandlerMap();
	}

	private void initNodeHandlerMap() {
		nodeHandlerMap.put("trim", new TrimHandler());
		nodeHandlerMap.put("where", new WhereHandler());
		nodeHandlerMap.put("set", new SetHandler());
		nodeHandlerMap.put("foreach", new ForEachHandler());
		nodeHandlerMap.put("if", new IfHandler());
		nodeHandlerMap.put("choose", new ChooseHandler());
		nodeHandlerMap.put("when", new IfHandler());
		nodeHandlerMap.put("otherwise", new OtherwiseHandler());
		nodeHandlerMap.put("bind", new BindHandler());
	}

这就是不同凡响?可能有人是怀疑的,但是你看看initNodeHandlerMap方法中存了这么多对象,这些对象基本上都是mapper文件中的一些where、foreach、if等这些标签对象。因此我想着nodeHandlerMap肯定不是个普通的map集合。那我们来简单的看下nodeHandlerMap:

private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

private interface NodeHandler {
	void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}

private class BindHandler implements NodeHandler {
	public BindHandler() {
		// Prevent Synthetic Access
	}

	@Override
	public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
		final String name = nodeToHandle.getStringAttribute("name");
		final String expression = nodeToHandle.getStringAttribute("value");
		final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
		targetContents.add(node);
	}
}

private class WhereHandler implements NodeHandler {
	public WhereHandler() {
		// Prevent Synthetic Access
	}

	@Override
	public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
                // 混合SQL节点
		MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
		WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
		targetContents.add(where);
	}
}

从上面的代码中可以看出,nodeHandlerMap的存储的是NodeHandler这么个内部接口对象,那么在initNodeHandlerMap方法中所存的对象肯定是去实现NodeHandler的。因此在这里列出了两个相关的实现类代码,对于WhereHandler实现的代码里的parseDynamicTags方法,接下来我们会进行说明,因为那些实现的对象中,大多数调用了这个方法。我们还是继续回头看下builder.parseScriptNode()。

二、解析动态SQL

builder.parseScriptNode()的调用还是在createSqlSource方法中,既然如此,那便看看这个parseScriptNode方法到底做了什么:

public SqlSource parseScriptNode() {
	// 获取解析过的SQL信息(解析了动态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方法来获得一个MixedSqlNode对象,接着开始判断这个SQL是否是动态的,判断的标准是如果包含${}和动态SQL语句,那么就是dynamic的SQL,否则就是RawSqlSource。可能有同学在想这个isDynamic是怎么来的,不要着急等下就会揭晓这个问题。

我们先来看下parseDynamicTags到底做了什么,然后分别看下DynamicSqlSource和RawSqlSource对象的构造方法。

protected MixedSqlNode parseDynamicTags(XNode node) {
	List<SqlNode> contents = new ArrayList<>();
	//获取<select>\<insert>等4个标签的子节点,子节点包括元素节点和文本节点
	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);
}

上面的代码逻辑还是比较简单的,在获得那些标签的子元素后,然后对其进行遍历,在便利的时候来判断是文本节点还是元素节点,在这里我们可以看到isDynamic被置为true的操作。这里最后把所有获取出来的数据放到集合中,然后在把这个集合放到MixedSqlNode这个对象中,然后返回。

刚才说了要看看,DynamicSqlSource和RawSqlSource类,其实这两个类构造方法的代码很简单:

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
	this.configuration = configuration;
	this.rootSqlNode = rootSqlNode;
}

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
	this(configuration, getSql(configuration, rootSqlNode), parameterType);
}

哎……这个RawSqlSource的构造函数好像不是很简单啊,里面调用了this,而且还调用了getSql方法,那就顺便看下:

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
	// 解析SQL语句
	SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
	// 获取入参类型
	Class<?> clazz = parameterType == null ? Object.class : parameterType;
	// 开始解析
	sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}

private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
	DynamicContext context = new DynamicContext(configuration, null);
	rootSqlNode.apply(context);
	return context.getSql();
}

貌似又发现了新大陆,在调this的方法里,首先创建了SqlSourceBuilder对象,然后获取parameterType,最后进行解析,这个解析我们等下再看,先看看getSql里做了什么,貌似并没做什么,就是先创建DynamicContext对象,最后通过动态上下文去获取SQL。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
	ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
			additionalParameters);
	// 创建分词解析器
	GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
	// 解析#{}
	String sql = parser.parse(originalSql);
	// 将解析之后的SQL信息,封装到StaticSqlSource对象中
	return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

这里好像已经到了挺核心的代码了,首先创建参数映射的分词Handler,然后创建分词解析器,最后通过分词解析器去解析#{},最后将解析之后的SQL信息,封装到StaticSqlSource对象中。

说到了这里,可能有人想那为什么DynamicSql就这么简单的结束了,我只能说先不要急,精彩的还在后面。到这里貌似已经动态SQL解析貌似结束了,可是我们貌似忽略了ParameterMappingTokenHandler中的handleToken(String content)和buildParameterMapping方法,因为我这里没贴parser.parse(originalSql)的parse方法代码,对于handleToken的方法就是在这个方法中调用的,然后在handleToken中调用buildParameterMapping。这块读者可以自己去看看。

三、创建MappedStatement对象

最后来看下前一篇没说的,就是创建MappedStatement对象。这个方法的入口和createSqlSource方法在同一个方法里,我们直接来看下是如何创建的,代码如下:

  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    //利用构建者模式,去创建MappedStatement.Builder,用于创建MappedStatement对象
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    // 通过MappedStatement.Builder,构建一个MappedStatement
    MappedStatement statement = statementBuilder.build();
    // 将MappedStatement对象存储到Configuration中的Map集合中,key为statement的id,value为MappedStatement对象
    configuration.addMappedStatement(statement);
    return statement;
  }

看上去很长的一段代码,其实不过是唬人的,这段代码中核心点在于首先利用构建者模式,去创建MappedStatement.Builder对象,最后通过MappedStatement.Builder,构建一个MappedStatement,其它的都是辅助作用而已。

至于MappedStatement.Builder(……)的代码大家可以自己去看,代码很简单,对于statementBuilder.build()的代码也很简单,在这里就不贴代码了,感兴趣的可以自己看看。

其实到这里就已经结束本篇文章了,笔者由于阅历有限,可能遗漏很多细节,大家可以给笔者留言,谢谢!那就最后来个总结!

总结

  • 相关类和接口:
    • XMLLanguageDriver
    • XMLScriptBuilder
    • SqlSource
    • SqlSourceBuilder
    • NodeHandler
    • ……
  • XMLLanguageDriver#createSqlSource:创建SqlSource
    • XMLScriptBuilder#构造方法:初始化动态SQL中的节点处理器集合
    • XMLScriptBuilder#parseScriptNode:
      • XMLScriptBuilder#parseDynamicTags:解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
      • DynamicSqlSource#构造方法:如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource中
      • RawSqlSource#构造方法:如果SQL中包含#{},则将SqlNode封装到RawSqlSource
        • ParameterMappingTokenHandler#构造方法
        • GenericTokenParser#构造方法:指定待分析的openTokencloseToken,并指定处理器
        • GenericTokenParser#parse:解析SQL语句,处理openTokencloseToken中的内容
          • ParameterMappingTokenHandler#handleToken:处理token#{}/${}
            • ParameterMappingTokenHandler#buildParameterMapping:创建ParameterMapping对象
        • StaticSqlSource#构造方法:将解析之后的SQL信息,封装到StaticSqlSource
  • MapperBuilderAssistant#addMappedStatement:创建MappedStatement对象(下篇文章进行解析)
    • MappedStatement.Builder#构造方法
    • MappedStatement#build方法:创建MappedStatement对象,并存储到Configuration对象中
发布了41 篇原创文章 · 获赞 8 · 访问量 4252

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/103152985