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();
}
到这里全局变量就解析完成了,那么如果在全局变量中没有找到对 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源 应的值该怎么办呢?例如我这里使用的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和RawSqlSourc Java开源项目【ali1024.coding.net/public/P7/Java/git】 e。到这里配置文件就解析完成了,后续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;
}
最后
由于篇幅限制,小编在此截出几张知识讲解的图解
String.valueOf(value); // issue #274 return “” instead of “null”
checkInjection(srtValue);
return srtValue;
}
最后
由于篇幅限制,小编在此截出几张知识讲解的图解
[外链图片转存中…(img-vH6OxWEj-1650011901011)]
[外链图片转存中…(img-qmNQjsOq-1650011901012)]
[外链图片转存中…(img-LOFOxxv3-1650011901013)]
[外链图片转存中…(img-jCZBi1DQ-1650011901014)]
[外链图片转存中…(img-gOrsyI7M-1650011901016)]