Mybatis源码解析:sql参数处理,原来可以这么简单-1,java项目技术栈

//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 !=

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

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} 这个例子,我们没有在全局变量中定义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 additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", “}”, handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这里用的hander是ParameterMappingTokenHandler,它的作用是将#{XXX}替换成 ?

ParameterMappingTokenHandler.handleToken

public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return “?”;
}
这时sql就变成了select * from user where id = ?,到这里还只是解析配置文件。在具体执行方法时也要调用getBoundSql方法将参数进行赋值

//RawSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
StaticSqlSource.getBoundSql,最后调用BoundSql的构造方法,将sql语句,入参等传入

public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
之后就要创建数据库连接,进行查询了。回到这个方法SimpleExecutor.prepareStatement。回顾一下,这是创建StatementHandler后做的一些连接数据库的准备操作。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取jdbc数据库连接
Connection connection = getConnection(statementLog);
//一些准备工作,初始化Statement连接
stmt = handler.prepare(connection, transaction.getTimeout());
//使用ParameterHandler处理入参
handler.parameterize(stmt);
return stmt;
}
我们先进入这个方法PreparedStatementHandler.parameterize。

为什么是PreparedStatementHandler之前也说过,因为语句的默认类型是PREPARED, 还有其他的类型如果是CALLABLE,对应CallableStatementHandler,STATEMENT对应SimpleStatementHandler。可以用参数statementType进行设置。

@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
DefaultParameterHandler.setParameters.

@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity(“setting parameters”).object(mappedStatement.getParameterMap().getId());
//boundSql用来解析我们的sql语句,parameterMappings是我们传入的参数
List parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
//这里第一个参数就是id
ParameterMapping parameterMapping = parameterMappings.get(i);
//mode属性允许能指定IN,OUT或INOUT参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。
//#{id}默认mode为OUT
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
//这里是boundsql中的额外参数,可以使用拦截器添加,例子放在下文
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
//如果类型处理器中有这个类型,那么直接赋值就行了,例如这里是Integer类型,类型处理器是有的
//那么直接赋值
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//如果不是的会转化为元数据进行处理,metaObject元数据可以理解为用来反射的工具类,可以处理参数的get,set
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//获取类型处理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//获取数据库类型
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//使用不同的类型处理器向jdbc中的PreparedStatement设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
value如果是空的那么就直接设置为jdbc的空类型,不为空调用具体的类型处理器。

BaseTypeHandler.setParameter。该类是所有typeHandler的父类.如果不为空调用setNonNullParameter,该方法时抽象的,由具体的子类实现。这里使用的是一个相当于路由的的子类UnknownTypeHandler,这个子类可以根据传入的类型,再去找到具体的类型处理器,例如IntegerTypeHander.

@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {

你可能感兴趣的:(程序员,面试,java,后端)