一文搞懂 Mybatis 究竟是如何解析SQL语句(上)
前言:
本文紧接着 Mybatis是如何创建出SqlSessionFactory 一文进行详细分析,在上一篇文章中主要描述了 SqlSessionFactory
被创建的执行逻辑,那么在本文中,我们将详细探讨在创建出 SqlSessionFactory
的逻辑中,是如何解析 SQL
语句进行绑定的。那么本文中主要讲解上一篇文章 6
小节中 mapperElement(root.evalNode("mappers"));
这一句代码执行的逻辑,那么如果还没有了解 SqlSessionFactory
是如何被创建出来的,那么就需要打打基础了。
一、解析 mappers
标签
在 Mybatis
全局配置文件 mybatis-config.xml
中,配置了 <mappers
> 标签,主要作用是为了让 mybatis
框架知道在启动的时候应该扫描那个文件,用户自定义的 SQL
放入哪个文件中,以下就是 mybatis-config.xml
内 mappers
标签内容:
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
从 Mybatis是如何创建出SqlSessionFactory 可以了解到,Mybatis
主要是以靠 mybatis-config.xml
进行解析,那么接下来,我们进入 org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement(org.apache.ibatis.parsing.XNode)
方法中,探讨以下 UserMapper.xml
中是如何被解析为 SQL
并进行参数绑定的
UserMapper.xml
读取
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.onebatis.UserMapper">
<select id="getUserPage" resultType="com.example.onebatis.User">
select id, user_name, password, address, phone from t_user where phone = #{phone}
</select>
</mapper>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 不同的定义方式的扫描,最终都是调用 addMapper()方法(添加到 MapperRegistry)。这个方法和 getMapper() 对应
// package 包
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 获取 resource 属性 UserMapper.xml
String resource = child.getStringAttribute("resource");
// 获取 url 属性
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 根据配置,我们配置的是 resource 属性,那么这里的 resource 一定不为空
// url 没有配置,url == null
// mapperClass 也没有配置,mapperClass == null
if (resource != null && url == null && mapperClass == null) {
// **********************被关注的逻辑***************************
// resource 相对路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析 Mapper.xml,总体上做了两件事情 >>
mapperParser.parse();
// *********************被关注的逻辑****************************
}
// 不走此处逻辑
else if (resource == null && url != null && mapperClass == null) {
// url 绝对路径
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 单个接口
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.");
}
}
}
}
}
根据上面代码中的注释,我们可以很清晰的根据配置文件得到被执行的逻辑,那么我将被执行的逻辑代码片段重新提取。这里对对上面方法进行总结,以上的方法,主要是拿到 mybatis-config.xml
中 mappers
节点数据,通过不同类型的定义进行不同方式的加载扫描,第一种 package
,第二种 resource 被定义,第三种 url 被定义,第四种 mapperClass
被定义,最终无论走的是哪一种逻辑,那么都会定位到我们自己配置的 UserMapper.xml
文件,其余三种方式这里不做详细解释。
// resource 是被加载到的文件名字,因为我们放到了 classpath 下,所以没有任何其他目录,所以当前 resouce = UserMapper.xml
// 这里进行读取文件,想要自己实现,其实可以使用 hutool 工具包中 ClasspathUtil 实现,将文件读成 Stream
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建一个 mapper xml 的解析器
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 调用解析器的 parse 方法对mapper xml 进行解析, 1.2 小节将讲解此方法
mapperParser.parse();
二、UserMapper.xml 解析
根据上述内容,大致知道了 Mapper xml
的加载流程,在本文中我们不关心它究竟是如何被加载的,我们只关心是如何被解析的 SQL
,那么接下来进入 mapperParser.parse()
, 代码如下:
public void parse() {
// 先检查 xml 是否已经被加载过了,如果被加载过了,就不需要进行加载了
if (!configuration.isResourceLoaded(resource)) {
// 开始解析 mapper 标签,那么这个 <mapper> 标签就是 UserMapper.xml 的顶层标签
configurationElement(parser.evalNode("/mapper"));
// 解析完毕后,将 UserMapper.xml 进行标记已解析
configuration.addLoadedResource(resource);
// 把namespace(接口类型)和工厂类绑定起来,放到一个map。
// 一个namespace 一个 MapperProxyFactory >>
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
那么上述逻辑中,很清晰的就能看出, mapper
标签是 UserMapper.xml
中顶层标签,既然这里标记解析顶层标签,那么子标签的解析一定都在 configurationElement(XNode)
中,下面的代码进入此方法内:
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");
}
// mapper -> 父标签
// cache -> 子标签
// parameterMap ... -> 子标签
// insert/delete/update/select -> 子标签
builderAssistant.setCurrentNamespace(namespace);
// 添加缓存对象
cacheRefElement(context.evalNode("cache-ref"));
// 解析 cache 属性,添加缓存对象
cacheElement(context.evalNode("cache"));
// 创建 ParameterMapping 对象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 创建 List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可以复用的SQL
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增删改查标签,得到 MappedStatement ***
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);
}
}
这里不难看出,每个括号中定义的字符串,都是属于 UserMapper.xml 的标签
2.1 解析 CRUD
标签
1.2 小节最后一段代码中,不仅解析 CRUD
标签,而且还真针对 cache-ref
、cahe
、paramterMap
、resultMap
、sql
标签进行了解析,在本文中都不进行展开,那么我们主要针对 select
、insert
、update
、delete
标签的解析进行解读,进入 buildStatementFromContext()
方法,代码如下:
private void buildStatementFromContext(List<XNode> list) {
// 这里我们并没有配置任何 databaseId 属性,那么这里一定为 null,所以 if 条件不成立
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 这里就进入了解析逻辑
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 用来解析增删改查标签的 XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析 Statement,添加 MappedStatement 对象 >>
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
这里要解释以下,为什么解析方法被命名为 buildStatemenet
,而不是其他的名字。在 mybatis
中,CRUD
标签被称为 statement
, 而 statement
被翻译为中文标识 <声明、清单…>,那么我们作为使用 mybatis
的用户,针对 mybatis
可以理解为员工,我们要他给我干活,给他指明它要干的活,那么它自己就需要知道具体要干哪些活,列出清单。这里解释有点牵强。
Mybatis
解析 statement
有专有的 XMLStatementBuilder
解析器进行解析,从上述代码中,直观就能看出 statementParse.parseStatementNode()
方法就是解析的核心,进入方法代码如下:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
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);
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
processSelectKeyNodes(id, parameterTypeClass, langDriver);
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;
}
// ********************本文核心代码***********************
// 在本句代码中,对SQL进行了解析,那么这句代码则是众多代码中的核心语句,其他的我们都暂时不关心
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// ********************本文核心代码***********************
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
2.2 创建SQL解析器
进入代码 langDriver.createSqlSource()
中,LanguageDriver
对象是接口,本文中我们主要是使用了 XML
方式,所以毫无疑问进入 XMLLanguageDriver
的 creteSqlSource()
,代码如下:
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 创建一个 xml 脚本解析器
XMLScriptBuilder builder = new XMLScriptBuilder(configuration,
script, parameterType);
// 开始解析 node 节点
return builder.parseScriptNode();
}
// 本方法是 XMLScriptBuilder 类中的
public SqlSource parseScriptNode() {
// 此句代码主要完成逻辑
// 1. 如果标签内没有子标签的情况,将占位符 ${} 解析为 ?
// 2. 如果标签内,包含了 Element 类型的标签,那么将不同类型的
// element 交由不同类型的处理器进行解析,解析完毕后返回,并标识sql是否是动态,
// 将此类成员变量 isDynamic 进行修改
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 静态sql,将 #{} 替换为 ?
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
通过以上的代码 parseDynamicTags(context)
方法,主要作用注释在了代码内,那我我们来一探究竟,到底事如何将 ${}
转换为 ?
的,中途又有其他哪些操作,作用是什么?
2.3 区分静态SQL与动态SQL
// 该方法会被递归调用,请注意
// 当前 XNode 对象表示 curd 标签
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 拿到所有的子标签,例如 <trim> <where> <set> <foreach> <if> <when> ...
// 此句代码,如果获取不到字表单的时候,NodeList里面依然有一个值
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 将获取到子节点变为 XNode 对象
XNode child = node.newXNode(children.item(i));
// 判断当前节点有没有包含子节点
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 拿到 标签内定义的 SQL 语句
String data = child.getStringBody("");
// 进入解析 SQL 核心代码
TextSqlNode textSqlNode = new TextSqlNode(data);
// 判断 SQL 是不是动态sql语句,如果是,则把 Node 对象装入
// 集合中,等待下一次的处理
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
}
// 直接将 SQL 置为静态SQL,那么这里就不需要做任何处理
else {
contents.add(new StaticTextSqlNode(data));
}
}
// 包含了字节点,例如<set> <where> ... 等标签
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
那么,mybatis
是如何判断一条sql
语句是否是动态sql
语句的,接下来,我们进入 textSqlNode.isDynamic()
方法内,由于文章篇幅问题,我这里就将下面的部分挪到了第二篇文章。
三、总结(上)
mybatis
在解析 SQL
语句的时候,区分为 静态SQL
与 动态SQL
,那么评判 静态SQL
与 动态SQL
的标准是什么?
在
parseDynamicTags(XNode node)
代码中,它首先去读取了select (这里用select标签举例)标签内所有的子节点,然后判断每一个节点内是否包含了真正的 XML 标签,如果没有包含,那么就将其定义为静态sql,如果包含了例如<where> <set> <if> <foreach>
等标签,那么它一定是动态SQL,既然是动态的SQL,那么就会调用不同的动态SQL标签的处理器,分别处理。在类XMLScriptBuilder
的构造方法中,将所有可能存在的标签,进行对处理器的初始化。
在理解 Mybatis
源码的时候,一定要注意一个事项,除了 Configuration
对象外,绝大多数对象都是与每一个 SQL
语句相关的,意思就是不同的 SQL
语句,所持有对象的地址是不同的,就例如 XMLScriptBuilder
对象,每次 CRUD
标签都会对应一个不同的对象,而不同的对象内不同标签的处理器实例也不同。