Mybatis源码解析:sql参数处理,原来可以这么简单-1(1)

//解析节点里面的内容,也就是sql了
this.body = parseBody(node);
}
variables传入配置中的全局变量

//XNode.parseBody
private String parseBody(Node node) {
//获取当前节点的信息
//例子中这里返回空
String data = getBodyData(node);
if (data == null) {
//获取孩子节点的信息
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
//获取当前节点的信息
data = getBodyData(child);
if (data != null) {
break;
}
}
}
return data;
}
为什么select节点getBodyData会返回空呢,从它的方法体中可以看出,首先它会判断节点的类型,select这个节点是ELEMENT_NODE类型,不属于它要求的文本类型或者部分节点类型。那么就直接返回空了。而当select的孩子节点,也就是sql语句select * from user where id = ${globalId}这个节点调用getBodyData方法时,sql语句是文本类型的,满足条件,才会使用解析器开始解析。

//XNode.getBodyData
private String getBodyData(Node child) {
//判断节点的类型
if (child.getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNodeType() == Node.TEXT_NODE) {
String data = ((CharacterData) child).getData();
data = PropertyParser.parse(data, variables);
return data;
}
return null;
}
//PropertyParser.parse
public static String parse(String string, Properties variables) {
//先创建一个处理器
VariableTokenHandler handler = new VariableTokenHandler(variables);
//创建解析器
GenericTokenParser parser = new GenericTokenParser(“${”, “}”, handler);
//进行解析
return parser.parse(string);
}
这里出现了很多陌生的类。首先是GenericTokenParser通用类型的解析器,他能根据传入的参数做出相应。如果参数满足条件,就会调用handler处理器来处理参数。每个handler都要实现handleToken方法,该方法就是用来处理参数的。

例如这里传入的是以${作为开头,}作为结尾。如果传入的字符串包含一个或者多个这样的格式,就会调用VariableTokenHandler.handleToken,该方法会试图从全局中找到该变量,并修改成具体的值。

VariableTokenHandler.handleToken 传入String变量globalId,将其替换成1并返回。

public String handleToken(String content) {
//variables里面存放全局的变量,为空直接return
if (variables != null) {
String key = content;
//是否存在默认值,默认是false
if (enableDefaultValue) {
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
//variables是用来存放全局变量的容器。
//这里会从全局变量中找到我们定义的globalId,然后将对应的值返回,这样我们的sql就拼接完成了
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
return “${” + content + “}”;
}
}

解析器代码,根据传入的标记开始解析,这里传入开始标记KaTeX parse error: Expected '}', got 'EOF' at end of input: {和结束标记}。在这之后还会用来解析#{}。代码比较长,最好打个断点进去看。

//GenericTokenParser.parse
public String parse(String text) {
if (text == null || text.isEmpty()) {
return “”;
}
//查找开始标记,如果不存在返回-1 ,存在返回偏移量
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
//这个变量用来存放中间的字符,如${id}中的id
StringBuilder expression = null;
//如果存在开始标志
while (start > -1) {
//这里将从offset开始,一直到start的字符先放入builder中
//例如select * from user where id =
if (start > 0 && src[start - 1] == ‘\’) {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let’s search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
//更新偏移量
offset = start + openToken.length();
//找到与开始标志对应的结束标志
int end = text.indexOf(closeToken, offset);
//取到中间字符globalId
while (end > -1) {
if (end > offset && src[end - 1] == ‘\’) {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//这里根据不同的处理器会有不同的操作,刚才传入的是VariableTokenHandler
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
到这里全局变量就解析完成了,那么如果在全局变量中没有找到对应的值该怎么办呢?例如我这里使用的sql是select * from user where id = i d ,而不是 {id},而不是 id,而不是{globalId},那么根据VariableTokenHandler处理器,它会原封不动的进行返回,等待后文的解析。

顺便一提,这一部分的解析实在解析我们的配置文件的时候就发生了,方法入口为context.evalNodes(“select|insert|update|delete”),在解析配置的时候,其他节点也大量使用了context.evalNodes()方法去解,所以只要当配置mybatis.xml文件中的properties节点解析完成之后,里面的变量就是能全局使用了,这也是为什么properties节点要放在第一个解析。

又由于这个通用解析器只解析${XXX}格式的变量,所以全局的变量不能写成#{xxx}.

入参${}的解析
List get(Integer id);

select * from user where id = i d < / s e l e c t > 这个例子 , 我们没有在全局变量中定义 i d , 而是在方法中传入这个值。根据上文中的 V a r i a b l e T o k e n H a n d l e r . h a n d l e T o k e n 方法就会返回 {id} </select> 这个例子,我们没有在全局变量中定义id,而是在方法中传入这个值。根据上文中的VariableTokenHandler.handleToken方法就会返回 id</select>这个例子,我们没有在全局变量中定义id,而是在方法中传入这个值。根据上文中的VariableTokenHandler.handleToken方法就会返回{id},表示这个参数全局变量中没有,是待解析的参数。

这是解析buildStatementFromContext(context.evalNodes(“select|insert|update|delete”));的后续代码,用来解析标签,并创建mappedStaement,在第二章中也分析过,这里直接copy过来.

//XMLStatementBuilder.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);
//是否需要处理嵌套查询结果 group by

// 三组数据 分成一个嵌套的查询结果
boolean resultOrdered = context.getBooleanAttribute(“resultOrdered”, false);

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//替换Includes标签为对应的sql标签里面的值
includeParser.applyIncludes(context.getNode());

String parameterType = context.getStringAttribute(“parameterType”);
Class<?> parameterTypeClass = resolveClass(parameterType);

//解析配置的自定义脚本语言驱动 mybatis plus
String lang = context.getStringAttribute(“lang”);
LanguageDriver langDriver = getLanguageDriver(lang);

// Parse selectKey after includes and remove them.
//解析selectKey
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: and were parsed and removed)
//设置主键自增规则
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 根据sql文本来判断是否需要动态解析 如果没有动态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”);
//引用外部 parameterMap,已废弃
String parameterMap = context.getStringAttribute(“parameterMap”);
//结果类型
String resultType = context.getStringAttribute(“resultType”);
Class<?> resultTypeClass = resolveClass(resultType);
//引用外部的 resultMap
String resultMap = context.getStringAttribute(“resultMap”);
//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute(“resultSetType”);
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
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);
}

找到解析sql的部分具体来分析,一层一层往下。

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
RawLanguageDriver.createSqlSource 该类是XMLLanguageDriver的子类

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}
XMLLanguageDriver.createSqlSource

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
XMLScriptBuilder.parseScriptNode

public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//判断节点是否是动态的,包含是否包含if、where 、choose、trim、foreach、bind、sql标签,这个例子中我们进入else
if (isDynamic) {
//不解析
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//用占位符方式来解析
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
这里进行判断isDynamic的值,这个方法我们只需要关注textSqlNode.isDynamic()就行了。代码与之前解析node有些类似。

protected MixedSqlNode parseDynamicTags(XNode node) {
List contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//注意!!这里又new了一个XNode,也就是说,这个节点中的sql语句又被解析了一次,解析方式和上文从同全局获取变量一样。
//与上文不同的是,这里传入的是子节点,也就是sql文本语句,而上文解析的是整个select元素
//这个child是临时变量,节点解析的结果不做保存
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 = 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);
}
TextSqlNode.isDynamic

public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
//这里创建一个解析器进行解析sql语句,这里解析的是仍然是KaTeX parse error: Expected 'EOF', got '}' at position 103: …r.isDynamic(); }̲ private Generi…{", “}”, handler);
}
熟悉的代码,还是同样的解析器,用来处理${,和},不过这次的hander不同,为DynamicCheckerTokenParser

//DynamicCheckerTokenParser.handleToken
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
这次的处理方式是将直接返回空,也就是说,sql会变成 select * from user where id = null。但是返回的结果并没有被保存,parser.parse(text)并没有参数来接受它的返回值,所以这里只是用来更新isDynamic参数。

回到XMLScriptBuilder.parseScriptNode方法,这里根据isDynamic的布尔值,会有两种SqlSource.DynamicSqlSource和RawSqlSource。到这里配置文件就解析完成了,后续sql中的参数都是从方法中获取的,所以只能在执行的时候动态进行替换。

来到query查询方法,方法在第三章执行sql的时候简单说过。ms.getBoundSql会获取绑定的封装sql.

//CachingExecutor.query
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MappedStatement.getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
//获取绑定的sql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//获取sql中对应的参数
List parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}

// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}

return boundSql;
}
//DynamicSqlSource.getBoundSql。
public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我们方法传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
//这里解析KaTeX parse error: Expected 'EOF', got '}' at position 441: …turn boundSql; }̲ 为什么是DynamicSql…{},使用的就是DynamicSqlSource。

//MixedSqlNode.apply
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
TextSqlNode.apply

@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
这里再次创建了${}的解析器,这次的handler是BindingTokenParser

public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}

private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("KaTeX parse error: Expected 'EOF', got '}' at position 20: …"}", handler); }̲ BindingTokenPa…{},就会将其替换成具体的参数,语句就变成 select * from user where id = 1,就能直接执行了

public String handleToken(String content) {
Object parameter = context.getBindings().get(“_parameter”);
if (parameter == null) {
context.getBindings().put(“value”, null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put(“value”, parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? “” : String.valueOf(value); // issue #274 return “” instead of “null”
checkInjection(srtValue);
return srtValue;
}
入参#{}的解析
那么如果是#{}该怎么处理呢?

select * from user where id = #{id} List get(Integer id); 由上文得知,由于没有${},那么SqlSource就会变成RawSqlSource。在创建RawSqlSource的时候,在构造方法中就会对#{}解析。

RawSqlSource的构造方法。

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

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
SqlSourceBuilder.parse

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值