select user_id,user_name from t_user where user_id = #{user_id}
10:27:20.247 [main] DEBUG william.mybatis.quickstart.mapper.UserMapper.selectById - ==> Preparing: select id, user_name from t_user where id = ?
10:27:20.285 [main] DEBUG william.mybatis.quickstart.mapper.UserMapper.selectById - ==> Parameters: 1(Long)
MyBatis对SQL语句解析的处理在XMLStatementBuilder类中,见源码:
/**
* 解析mapper中的SQL语句
*/
public void parseStatementNode() {
//SQL语句id,对应着Mapper接口的方法
String id = context.getStringAttribute("id");
//校验databaseId是否匹配
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//SQL标签属性解析
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType); //参数类型
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType); //结果类型
String resultSetType = context.getStringAttribute("resultSetType");
//Statement类型,默认PreparedStatement
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
//SQL命令类型:增删改查
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);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//重要:解析SQL语句,封装成一个SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
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;
}
//解析完毕,最后通过MapperBuilderAssistant创建MappedStatement对象,统一保存到Configuration的mappedStatements属性中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
前面是对SQL标签的一些处理,如id、缓存、结果集映射等。我们这次主要分析预编译机制,因此重点关注 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass)这个方法。这该方法会通过LanguageDriver对SQL语句进行解析,生成一个SqlSource。SqlSource封装了映射文件或者注解中定义的SQL语句,它不能直接交给数据库执行,因为里面可能包含动态SQL或者占位符等元素。而MyBatis在实际执行SQL语句时,会调用SqlSource的getBoundSql()方法获取一个BoundSql对象,BoundSql是将SqlSource中的动态内容经过处理后,返回的实际可执行的SQL语句,其中包含?占位符List封装的有序的参数映射关系,此外还有一些额外信息标识每个参数的属性名称等。
LanguageDriver的默认实现类是XMLLanguageDriver,我们进入到这个方法里面看下:
//创建SqlSource
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//创建XMLScriptBuilder对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//通过XMLScriptBuilder解析SQL脚本
return builder.parseScriptNode();
}
这里通过XMLScriptBuilder对象的parseScriptNode()方法进行SQL脚本的解析,继续跟进去:
/**
* 解析SQL脚本
*/
public SqlSource parseScriptNode() {
//解析动态标签,包括动态SQL和${}。执行后动态SQL和${}已经被解析完毕。
//此时SQL语句中的#{}还没有处理,#{}会在SQL执行时动态解析
MixedSqlNode rootSqlNode = parseDynamicTags(context);
//如果是dynamic的,则创建DynamicSqlSource,否则创建RawSqlSource
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
parseScriptNode的功能就是判断该SQL节点是否是动态的,然后根据是否动态返回DynamicSqlSource或
RawSqlSource。是否为动态SQL的判断在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));
//处理文本节点(SQL语句)
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
//把SQL封装到TextSqlNode
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//如果包含${},则是dynamic的
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
//除了${}外,其他的SQL都是静态的
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
在这个方法中,会对SQL语句进行动态标签的解析。以<select>标签为例,会获取标签中的文本节点(即具体的SQL语句),将其封装成TextSqlNode,然后调用isDynamic()方法判断是否为动态标签。那么我们来看看这个方法:
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
这里涉及一些底层的文本解析,这里就不具体说明了,我们仅需看下createParser()这个方法:
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
这样一来就明白了,该方法会创建一个以"
如果是动态标签,创建的SqlSource就是DynamicSqlSource,其获取的BoundSql就是直接进行字符串的替换。对于非动态标签,则创建RawSqlSource,对应?占位符的SQL语句,如前文所述。
select * from users where username='admin' and password=md5('admin')
select * from users where username='' or 1=1#' and password=md5('')
select * from users where username='' or 1=1
select * from users
select * from users where username=#{username} and password=md5(#{password})
select * from users where username=''or 1=1#' and password=md5('')